gl_engine: Fix for rendering short paths

Ensure they do not terminate prematurely for paths
with a step of 0 or exceptionally small values
when a valid stroke-width is present.

In my opinion, the optimal approach was to separate vertex generation
into dedicated methods: strokeRoundPoint and strokeSquarePoint.

My update supports two different stroke-cap styles.
I have also tested it with various files (JSON, SVG)
as well as a small example application similar
to the one included in the previous pull request (#3066).

issue: https://github.com/thorvg/thorvg/issues/3065
This commit is contained in:
Marcin Baszczewski 2024-12-20 10:36:18 +01:00 committed by Mira Grudzinska
parent d00b727cd4
commit 978464d87c
2 changed files with 82 additions and 20 deletions

View file

@ -1771,40 +1771,42 @@ void Stroker::doStroke(const PathCommand *cmds, uint32_t cmd_count, const Point
mResGlPoints->reserve(pts_count * 4 + 16); mResGlPoints->reserve(pts_count * 4 + 16);
mResIndices->reserve(pts_count * 3); mResIndices->reserve(pts_count * 3);
auto validStrokeCap = false;
for (uint32_t i = 0; i < cmd_count; i++) { for (uint32_t i = 0; i < cmd_count; i++) {
switch (cmds[i]) { switch (cmds[i]) {
case PathCommand::MoveTo: { case PathCommand::MoveTo: {
if (mStrokeState.hasMove) { if (validStrokeCap) { // check this, so we can skip if path only contains move instruction
strokeCap(); strokeCap();
mStrokeState.hasMove = false; validStrokeCap = false;
} }
mStrokeState.hasMove = true;
mStrokeState.firstPt = *pts; mStrokeState.firstPt = *pts;
mStrokeState.firstPtDir = GlPoint{}; mStrokeState.firstPtDir = GlPoint{};
mStrokeState.prevPt = *pts; mStrokeState.prevPt = *pts;
mStrokeState.prevPtDir = GlPoint{}; mStrokeState.prevPtDir = GlPoint{};
pts++; pts++;
validStrokeCap = false;
} break; } break;
case PathCommand::LineTo: { case PathCommand::LineTo: {
validStrokeCap = true;
this->strokeLineTo(*pts); this->strokeLineTo(*pts);
pts++; pts++;
} break; } break;
case PathCommand::CubicTo: { case PathCommand::CubicTo: {
validStrokeCap = true;
this->strokeCubicTo(pts[0], pts[1], pts[2]); this->strokeCubicTo(pts[0], pts[1], pts[2]);
pts += 3; pts += 3;
} break; } break;
case PathCommand::Close: { case PathCommand::Close: {
this->strokeClose(); this->strokeClose();
mStrokeState.hasMove = false; validStrokeCap = false;
} break; } break;
default: default:
break; break;
} }
} }
if (validStrokeCap) strokeCap();
strokeCap();
} }
void Stroker::doDashStroke(const PathCommand *cmds, uint32_t cmd_count, const Point *pts, uint32_t pts_count, void Stroker::doDashStroke(const PathCommand *cmds, uint32_t cmd_count, const Point *pts, uint32_t pts_count,
@ -1825,19 +1827,21 @@ void Stroker::doDashStroke(const PathCommand *cmds, uint32_t cmd_count, const Po
void Stroker::strokeCap() void Stroker::strokeCap()
{ {
if (mStrokeState.firstPt == mStrokeState.prevPt) {
return;
}
if (mStrokeCap == StrokeCap::Butt) return; if (mStrokeCap == StrokeCap::Butt) return;
else if (mStrokeCap == StrokeCap::Square) {
strokeSquare(mStrokeState.firstPt, GlPoint{-mStrokeState.firstPtDir.x, -mStrokeState.firstPtDir.y}); if (mStrokeCap == StrokeCap::Square) {
if (mStrokeState.firstPt == mStrokeState.prevPt) strokeSquarePoint(mStrokeState.firstPt);
else {
strokeSquare(mStrokeState.firstPt, {-mStrokeState.firstPtDir.x, -mStrokeState.firstPtDir.y});
strokeSquare(mStrokeState.prevPt, mStrokeState.prevPtDir); strokeSquare(mStrokeState.prevPt, mStrokeState.prevPtDir);
}
} else if (mStrokeCap == StrokeCap::Round) { } else if (mStrokeCap == StrokeCap::Round) {
strokeRound(mStrokeState.firstPt, GlPoint{-mStrokeState.firstPtDir.x, -mStrokeState.firstPtDir.y}); if (mStrokeState.firstPt == mStrokeState.prevPt) strokeRoundPoint(mStrokeState.firstPt);
else {
strokeRound(mStrokeState.firstPt, {-mStrokeState.firstPtDir.x, -mStrokeState.firstPtDir.y});
strokeRound(mStrokeState.prevPt, mStrokeState.prevPtDir); strokeRound(mStrokeState.prevPt, mStrokeState.prevPtDir);
} }
}
} }
void Stroker::strokeLineTo(const GlPoint &curr) void Stroker::strokeLineTo(const GlPoint &curr)
@ -1932,8 +1936,6 @@ void Stroker::strokeClose()
// join firstPt with prevPt // join firstPt with prevPt
this->strokeJoin(mStrokeState.firstPtDir); this->strokeJoin(mStrokeState.firstPtDir);
mStrokeState.hasMove = false;
} }
void Stroker::strokeJoin(const GlPoint &dir) void Stroker::strokeJoin(const GlPoint &dir)
@ -2035,6 +2037,33 @@ void Stroker::strokeRound(const GlPoint &prev, const GlPoint &curr, const GlPoin
} }
void Stroker::strokeRoundPoint(const GlPoint &p)
{
// Fixme: just use bezier curve to calculate step count
auto count = detail::_bezierCurveCount(detail::_bezFromArc(p, p, strokeRadius())) * 2;
auto c = detail::_pushVertex(mResGlPoints, p.x, p.y);
auto step = 2 * M_PI / (count - 1);
for (uint32_t i = 1; i <= static_cast<uint32_t>(count); i++) {
float angle = i * step;
GlPoint dir = {cos(angle), sin(angle)};
GlPoint out = p + dir * strokeRadius();
auto oi = detail::_pushVertex(mResGlPoints, out.x, out.y);
if (oi > 1) {
mResIndices->push(c);
mResIndices->push(oi);
mResIndices->push(oi - 1);
}
}
mLeftTop.x = std::min(mLeftTop.x, p.x - strokeRadius());
mLeftTop.y = std::min(mLeftTop.y, p.y - strokeRadius());
mRightBottom.x = std::max(mRightBottom.x, p.x + strokeRadius());
mRightBottom.y = std::max(mRightBottom.y, p.y + strokeRadius());
}
void Stroker::strokeMiter(const GlPoint &prev, const GlPoint &curr, const GlPoint &center) void Stroker::strokeMiter(const GlPoint &prev, const GlPoint &curr, const GlPoint &center)
{ {
auto pp1 = prev - center; auto pp1 = prev - center;
@ -2118,6 +2147,36 @@ void Stroker::strokeSquare(const GlPoint& p, const GlPoint& outDir)
} }
void Stroker::strokeSquarePoint(const GlPoint& p)
{
auto offsetX = Point{strokeRadius(), 0.0f};
auto offsetY = Point{0.0f, strokeRadius()};
auto a = p + offsetX + offsetY;
auto b = p - offsetX + offsetY;
auto c = p - offsetX - offsetY;
auto d = p + offsetX - offsetY;
auto ai = detail::_pushVertex(mResGlPoints, a.x, a.y);
auto bi = detail::_pushVertex(mResGlPoints, b.x, b.y);
auto ci = detail::_pushVertex(mResGlPoints, c.x, c.y);
auto di = detail::_pushVertex(mResGlPoints, d.x, d.y);
mResIndices->push(ai);
mResIndices->push(bi);
mResIndices->push(ci);
mResIndices->push(ci);
mResIndices->push(di);
mResIndices->push(ai);
mLeftTop.x = std::min(mLeftTop.x, std::min(std::min(a.x, b.x), std::min(c.x, d.x)));
mLeftTop.y = std::min(mLeftTop.y, std::min(std::min(a.y, b.y), std::min(c.y, d.y)));
mRightBottom.x = std::max(mRightBottom.x, std::max(std::max(a.x, b.x), std::max(c.x, d.x)));
mRightBottom.y = std::max(mRightBottom.y, std::max(std::max(a.y, b.y), std::max(c.y, d.y)));
}
void Stroker::strokeRound(const GlPoint& p, const GlPoint& outDir) void Stroker::strokeRound(const GlPoint& p, const GlPoint& outDir)
{ {
GlPoint normal{-outDir.y, outDir.x}; GlPoint normal{-outDir.y, outDir.x};

View file

@ -105,7 +105,6 @@ class Stroker final
GlPoint firstPtDir = {}; GlPoint firstPtDir = {};
GlPoint prevPt = {}; GlPoint prevPt = {};
GlPoint prevPtDir = {}; GlPoint prevPtDir = {};
bool hasMove = false;
}; };
public: public:
Stroker(Array<float>* points, Array<uint32_t>* indices, const Matrix& matrix); Stroker(Array<float>* points, Array<uint32_t>* indices, const Matrix& matrix);
@ -144,7 +143,11 @@ private:
void strokeSquare(const GlPoint& p, const GlPoint& outDir); void strokeSquare(const GlPoint& p, const GlPoint& outDir);
void strokeSquarePoint(const GlPoint& p);
void strokeRound(const GlPoint& p, const GlPoint& outDir); void strokeRound(const GlPoint& p, const GlPoint& outDir);
void strokeRoundPoint(const GlPoint& p);
private: private:
Array<float>* mResGlPoints; Array<float>* mResGlPoints;
Array<uint32_t>* mResIndices; Array<uint32_t>* mResIndices;