renderer: refactored to share gl stroke dasher among engines

Move stroke dasher to the common code space to create an abillity
to use it on the cross API renderers (wg and gl). Stroke dasher is
a path-to-path operation, same as path trim, so can be placed to
the common space.

Now RenderShape can generate dashed path by itself. Dashing mechanics
fully hiden from the user but can be used in gl and wg renderers.

issue: https://github.com/thorvg/thorvg/issues/3557

Co-Authored-By: Hermet Park <hermet@lottiefiles.com>
This commit is contained in:
Sergii Liebodkin 2025-07-17 12:09:56 +03:00 committed by Hermet Park
parent d8bbb0df31
commit 371f69b845
5 changed files with 249 additions and 278 deletions

View file

@ -28,18 +28,16 @@
bool GlGeometry::tesselate(const RenderShape& rshape, RenderUpdateFlag flag)
{
const RenderPath* path = nullptr;
RenderPath trimmedPath;
if (rshape.trimpath()) {
if (!rshape.stroke->trim.trim(rshape.path, trimmedPath)) return true;
path = &trimmedPath;
} else path = &rshape.path;
if (flag & (RenderUpdateFlag::Color | RenderUpdateFlag::Gradient | RenderUpdateFlag::Transform | RenderUpdateFlag::Path)) {
fill.clear();
BWTessellator bwTess{&fill};
bwTess.tessellate(*path, matrix);
if (rshape.trimpath()) {
RenderPath trimmedPath;
if (rshape.stroke->trim.trim(rshape.path, trimmedPath)) bwTess.tessellate(trimmedPath, matrix);
else return true;
} else bwTess.tessellate(rshape.path, matrix);
fillRule = rshape.rule;
bounds = bwTess.bounds();
}
@ -48,7 +46,7 @@ bool GlGeometry::tesselate(const RenderShape& rshape, RenderUpdateFlag flag)
stroke.clear();
Stroker stroker{&stroke, matrix};
stroker.stroke(&rshape, *path);
stroker.stroke(&rshape);
bounds = stroker.bounds();
}

View file

@ -39,7 +39,7 @@ Stroker::Stroker(GlGeometryBuffer* buffer, const Matrix& matrix) : mBuffer(buffe
}
void Stroker::stroke(const RenderShape *rshape, const RenderPath& path)
void Stroker::stroke(const RenderShape *rshape)
{
mMiterLimit = rshape->strokeMiterlimit();
mStrokeCap = rshape->strokeCap();
@ -52,9 +52,12 @@ void Stroker::stroke(const RenderShape *rshape, const RenderPath& path)
mStrokeWidth = strokeWidth / mMatrix.e11;
}
auto& dash = rshape->stroke->dash;
if (dash.length < DASH_PATTERN_THRESHOLD) doStroke(path);
else doDashStroke(path, dash.pattern, dash.count, dash.offset, dash.length);
RenderPath dashed;
if (rshape->strokeDash(dashed)) doStroke(dashed);
else if (rshape->trimpath()) {
RenderPath trimmedPath;
if (rshape->stroke->trim.trim(rshape->path, trimmedPath)) doStroke(trimmedPath);
} else doStroke(rshape->path);
}
@ -109,19 +112,6 @@ void Stroker::doStroke(const RenderPath& path)
}
void Stroker::doDashStroke(const RenderPath& path, const float *patterns, uint32_t patternCnt, float offset, float length)
{
RenderPath dpath;
dpath.cmds.reserve(20 * path.cmds.count);
dpath.pts.reserve(20 * path.pts.count);
DashStroke dash(&dpath.cmds, &dpath.pts, patterns, patternCnt, offset, length);
dash.doStroke(path, mStrokeCap != StrokeCap::Butt);
doStroke(dpath);
}
void Stroker::strokeCap()
{
if (mStrokeCap == StrokeCap::Butt) return;
@ -442,227 +432,6 @@ void Stroker::strokeRound(const Point& p, const Point& outDir)
}
DashStroke::DashStroke(Array<PathCommand> *cmds, Array<Point> *pts, const float *patterns, uint32_t patternCnt, float offset, float length)
: mCmds(cmds),
mPts(pts),
mDashPattern(patterns),
mDashCount(patternCnt),
mDashOffset(offset),
mDashLength(length)
{
}
void DashStroke::doStroke(const RenderPath& path, bool validPoint)
{
//validPoint: zero length segment with non-butt cap still should be rendered as a point - only the caps are visible
int32_t idx = 0;
auto offset = mDashOffset;
bool gap = false;
if (!tvg::zero(mDashOffset)) {
auto length = (mDashCount % 2) ? mDashLength * 2 : mDashLength;
offset = fmodf(offset, length);
if (offset < 0) offset += length;
for (uint32_t i = 0; i < mDashCount * (mDashCount % 2 + 1); ++i, ++idx) {
auto curPattern = mDashPattern[i % mDashCount];
if (offset < curPattern) break;
offset -= curPattern;
gap = !gap;
}
idx = idx % mDashCount;
}
auto pts = path.pts.data;
ARRAY_FOREACH(cmd, path.cmds) {
switch (*cmd) {
case PathCommand::Close: {
this->dashLineTo(mPtStart, validPoint);
break;
}
case PathCommand::MoveTo: {
// reset the dash state
mCurrIdx = idx;
mCurrLen = mDashPattern[idx] - offset;
mCurOpGap = gap;
mMove = true;
mPtStart = mPtCur = *pts;
pts++;
break;
}
case PathCommand::LineTo: {
this->dashLineTo(*pts, validPoint);
pts++;
break;
}
case PathCommand::CubicTo: {
this->dashCubicTo(pts[0], pts[1], pts[2], validPoint);
pts += 3;
break;
}
default: break;
}
}
}
void DashStroke::drawPoint(const Point& p)
{
if (mMove || mDashPattern[mCurrIdx] < FLOAT_EPSILON) {
this->moveTo(p);
mMove = false;
}
this->lineTo(p);
}
void DashStroke::dashLineTo(const Point& to, bool validPoint)
{
auto len = length(mPtCur - to);
if (tvg::zero(len)) {
this->moveTo(mPtCur);
} else if (len <= mCurrLen) {
mCurrLen -= len;
if (!mCurOpGap) {
if (mMove) {
this->moveTo(mPtCur);
mMove = false;
}
this->lineTo(to);
}
} else {
Line curr = {mPtCur, to};
while (len - mCurrLen > DASH_PATTERN_THRESHOLD) {
Line right;
if (mCurrLen > 0.0f) {
Line left;
curr.split(mCurrLen, left, right);
len -= mCurrLen;
if (!mCurOpGap) {
if (mMove || mDashPattern[mCurrIdx] - mCurrLen < FLOAT_EPSILON) {
this->moveTo(left.pt1);
mMove = false;
}
this->lineTo(left.pt2);
}
} else {
if (validPoint && !mCurOpGap) drawPoint(curr.pt1);
right = curr;
}
mCurrIdx = (mCurrIdx + 1) % mDashCount;
mCurrLen = mDashPattern[mCurrIdx];
mCurOpGap = !mCurOpGap;
curr = right;
mPtCur = curr.pt1;
mMove = true;
}
mCurrLen -= len;
if (!mCurOpGap) {
if (mMove) {
this->moveTo(curr.pt1);
mMove = false;
}
this->lineTo(curr.pt2);
}
if (mCurrLen < 0.1f) {
mCurrIdx = (mCurrIdx + 1) % mDashCount;
mCurrLen = mDashPattern[mCurrIdx];
mCurOpGap = !mCurOpGap;
}
}
mPtCur = to;
}
void DashStroke::dashCubicTo(const Point& cnt1, const Point& cnt2, const Point& end, bool validPoint)
{
Bezier cur{ mPtCur, cnt1, cnt2, end };
auto len = cur.length();
if (tvg::zero(len)) {
this->moveTo(mPtCur);
} else if (len <= mCurrLen) {
mCurrLen -= len;
if (!mCurOpGap) {
if (mMove) {
this->moveTo(mPtCur);
mMove = false;
}
this->cubicTo(cnt1, cnt2, end);
}
} else {
while (len - mCurrLen > DASH_PATTERN_THRESHOLD) {
Bezier right;
if (mCurrLen > 0.0f) {
Bezier left;
cur.split(mCurrLen, left, right);
len -= mCurrLen;
if (!mCurOpGap) {
if (mMove || mDashPattern[mCurrIdx] - mCurrLen < FLOAT_EPSILON) {
this->moveTo(left.start);
mMove = false;
}
this->cubicTo(left.ctrl1, left.ctrl2, left.end);
}
} else {
if (validPoint && !mCurOpGap) drawPoint(cur.start);
right = cur;
}
mCurrIdx = (mCurrIdx + 1) % mDashCount;
mCurrLen = mDashPattern[mCurrIdx];
mCurOpGap = !mCurOpGap;
cur = right;
mPtCur = cur.start;
mMove = true;
}
mCurrLen -= len;
if (!mCurOpGap) {
if (mMove) {
this->moveTo(cur.start);
mMove = false;
}
this->cubicTo(cur.ctrl1, cur.ctrl2, cur.end);
}
if (mCurrLen < 0.1f) {
mCurrIdx = (mCurrIdx + 1) % mDashCount;
mCurrLen = mDashPattern[mCurrIdx];
mCurOpGap = !mCurOpGap;
}
}
mPtCur = end;
}
void DashStroke::moveTo(const Point& pt)
{
mPts->push(pt);
mCmds->push(PathCommand::MoveTo);
}
void DashStroke::lineTo(const Point& pt)
{
mPts->push(pt);
mCmds->push(PathCommand::LineTo);
}
void DashStroke::cubicTo(const Point& cnt1, const Point& cnt2, const Point& end)
{
mPts->push(cnt1);
mPts->push(cnt2);
mPts->push(end);
mCmds->push(PathCommand::CubicTo);
}
BWTessellator::BWTessellator(GlGeometryBuffer* buffer): mBuffer(buffer)
{
}

View file

@ -39,12 +39,11 @@ class Stroker
};
public:
Stroker(GlGeometryBuffer* buffer, const Matrix& matrix);
void stroke(const RenderShape *rshape, const RenderPath& path);
void stroke(const RenderShape *rshape);
RenderRegion bounds() const;
private:
void doStroke(const RenderPath& path);
void doDashStroke(const RenderPath& path, const float* patterns, uint32_t patternCnt, float offset, float length);
float strokeRadius() const
{
@ -75,34 +74,6 @@ private:
Point mRightBottom = {0.0f, 0.0f};
};
class DashStroke
{
public:
DashStroke(Array<PathCommand>* cmds, Array<Point>* pts, const float* patterns, uint32_t patternCnt, float offset, float length);
void doStroke(const RenderPath& path, bool drawPoint);
private:
void drawPoint(const Point& p);
void dashLineTo(const Point& pt, bool drawPoint);
void dashCubicTo(const Point& pt1, const Point& pt2, const Point& pt3, bool drawPoint);
void moveTo(const Point& pt);
void lineTo(const Point& pt);
void cubicTo(const Point& pt1, const Point& pt2, const Point& pt3);
Array<PathCommand>* mCmds;
Array<Point>* mPts;
const float* mDashPattern;
uint32_t mDashCount;
float mDashOffset;
float mDashLength;
float mCurrLen = 0.0f;
int32_t mCurrIdx = 0;
bool mCurOpGap = false;
bool mMove = true;
Point mPtStart = {};
Point mPtCur = {};
};
class BWTessellator
{
public:

View file

@ -587,4 +587,210 @@ bool RenderTrimPath::trim(const RenderPath& in, RenderPath& out) const
}
return out.pts.count >= 2;
}
/************************************************************************/
/* StrokeDashPath Class Implementation */
/************************************************************************/
//TODO: use this common function from all engines
#ifdef THORVG_GL_RASTER_SUPPORT
struct StrokeDashPath
{
public:
StrokeDashPath(RenderStroke::Dash dash) : dash(dash) {}
bool gen(const RenderPath& in, RenderPath& out, bool drawPoint);
private:
void lineTo(RenderPath& out, const Point& pt, bool drawPoint);
void cubicTo(RenderPath& out, const Point& pt1, const Point& pt2, const Point& pt3, bool drawPoint);
void point(RenderPath& out, const Point& p);
template<typename Segment, typename LengthFn, typename SplitFn, typename DrawFn, typename PointFn>
void segment(Segment seg, float len, RenderPath& out, bool allowDot, LengthFn lengthFn, SplitFn splitFn, DrawFn drawFn, PointFn getStartPt, const Point& endPos);
RenderStroke::Dash dash;
float curLen = 0.0f;
int32_t curIdx = 0;
Point curPos{};
bool opGap = false;
bool move = true;
};
template<typename Segment, typename LengthFn, typename SplitFn, typename DrawFn, typename PointFn>
void StrokeDashPath::segment(Segment seg, float len, RenderPath& out, bool allowDot, LengthFn lengthFn, SplitFn splitFn, DrawFn drawFn, PointFn getStartPt, const Point& end)
{
#define MIN_CURR_LEN_THRESHOLD 0.1f
if (tvg::zero(len)) {
out.moveTo(curPos);
} else if (len <= curLen) {
curLen -= len;
if (!opGap) {
if (move) {
out.moveTo(curPos);
move = false;
}
drawFn(seg);
}
} else {
Segment left, right;
while (len - curLen > DASH_PATTERN_THRESHOLD) {
if (curLen > 0.0f) {
splitFn(seg, curLen, left, right);
len -= curLen;
if (!opGap) {
if (move || dash.pattern[curIdx] - curLen < FLOAT_EPSILON) {
out.moveTo(getStartPt(left));
move = false;
}
drawFn(left);
}
} else {
if (allowDot && !opGap) point(out, getStartPt(seg));
right = seg;
}
curIdx = (curIdx + 1) % dash.count;
curLen = dash.pattern[curIdx];
opGap = !opGap;
seg = right;
curPos = getStartPt(seg);
move = true;
}
curLen -= len;
if (!opGap) {
if (move) {
out.moveTo(getStartPt(seg));
move = false;
}
drawFn(seg);
}
if (curLen < MIN_CURR_LEN_THRESHOLD) {
curIdx = (curIdx + 1) % dash.count;
curLen = dash.pattern[curIdx];
opGap = !opGap;
}
}
curPos = end;
}
//allowDot: zero length segment with non-butt cap still should be rendered as a point - only the caps are visible
bool StrokeDashPath::gen(const RenderPath& in, RenderPath& out, bool allowDot)
{
int32_t idx = 0;
auto offset = dash.offset;
auto gap = false;
if (!tvg::zero(dash.offset)) {
auto length = (dash.count % 2) ? dash.length * 2 : dash.length;
offset = fmodf(offset, length);
if (offset < 0) offset += length;
for (uint32_t i = 0; i < dash.count * (dash.count % 2 + 1); ++i, ++idx) {
auto curPattern = dash.pattern[i % dash.count];
if (offset < curPattern) break;
offset -= curPattern;
gap = !gap;
}
idx = idx % dash.count;
}
auto pts = in.pts.data;
Point start{};
ARRAY_FOREACH(cmd, in.cmds) {
switch (*cmd) {
case PathCommand::Close: {
lineTo(out, start, allowDot);
break;
}
case PathCommand::MoveTo: {
// reset the dash state
curIdx = idx;
curLen = dash.pattern[idx] - offset;
opGap = gap;
move = true;
start = curPos = *pts;
pts++;
break;
}
case PathCommand::LineTo: {
lineTo(out, *pts, allowDot);
pts++;
break;
}
case PathCommand::CubicTo: {
cubicTo(out, pts[0], pts[1], pts[2], allowDot);
pts += 3;
break;
}
default: break;
}
}
return true;
}
void StrokeDashPath::point(RenderPath& out, const Point& p)
{
if (move || dash.pattern[curIdx] < FLOAT_EPSILON) {
out.moveTo(p);
move = false;
}
out.lineTo(p);
}
void StrokeDashPath::lineTo(RenderPath& out, const Point& to, bool allowDot)
{
Line line = {curPos, to};
auto len = length(to - curPos);
segment<Line>(line, len, out, allowDot,
[](const Line& l) { return length(l.pt2 - l.pt1); },
[](const Line& l, float len, Line& left, Line& right) { l.split(len, left, right); },
[&](const Line& l) { out.lineTo(l.pt2); },
[](const Line& l) { return l.pt1; },
to
);
}
void StrokeDashPath::cubicTo(RenderPath& out, const Point& cnt1, const Point& cnt2, const Point& end, bool allowDot)
{
Bezier curve = {curPos, cnt1, cnt2, end};
auto len = curve.length();
segment<Bezier>(curve, len, out, allowDot,
[](const Bezier& b) { return b.length(); },
[](const Bezier& b, float len, Bezier& left, Bezier& right) { b.split(len, left, right); },
[&](const Bezier& b) { out.cubicTo(b.ctrl1, b.ctrl2, b.end); },
[](const Bezier& b) { return b.start; },
end
);
}
#endif
bool RenderShape::strokeDash(RenderPath& out) const
{
if (!stroke || stroke->dash.count == 0 || stroke->dash.length < DASH_PATTERN_THRESHOLD) return false;
//TODO: use this common function from all engines
#ifdef THORVG_GL_RASTER_SUPPORT
out.cmds.reserve(20 * path.cmds.count);
out.pts.reserve(20 * path.pts.count);
StrokeDashPath dash(stroke->dash);
auto allowDot = stroke->cap != StrokeCap::Butt;
if (trimpath()) {
RenderPath tpath;
if (stroke->trim.trim(path, tpath)) return dash.gen(tpath, out, allowDot);
else return false;
}
return dash.gen(path, out, allowDot);
#else
return false;
#endif
}

View file

@ -233,6 +233,31 @@ struct RenderPath
cmds.clear();
}
void close()
{
cmds.push(PathCommand::Close);
}
void moveTo(const Point& pt)
{
pts.push(pt);
cmds.push(PathCommand::MoveTo);
}
void lineTo(const Point& pt)
{
pts.push(pt);
cmds.push(PathCommand::LineTo);
}
void cubicTo(const Point& cnt1, const Point& cnt2, const Point& end)
{
pts.push(cnt1);
pts.push(cnt2);
pts.push(end);
cmds.push(PathCommand::CubicTo);
}
bool bounds(Matrix* m, float* x, float* y, float* w, float* h);
};
@ -256,7 +281,7 @@ struct RenderStroke
float width = 0.0f;
RenderColor color{};
Fill *fill = nullptr;
struct {
struct Dash {
float* pattern = nullptr;
uint32_t count = 0;
float offset = 0.0f;
@ -383,6 +408,8 @@ struct RenderShape
if (!stroke) return 4.0f;
return stroke->miterlimit;;
}
bool strokeDash(RenderPath& out) const;
};
struct RenderEffect