diff --git a/inc/thorvg.h b/inc/thorvg.h index 0c13414e..a50f7ea0 100644 --- a/inc/thorvg.h +++ b/inc/thorvg.h @@ -1083,14 +1083,17 @@ public: /** * @brief Sets the dash pattern of the stroke. * - * @param[in] dashPattern The array of consecutive pair values of the dash length and the gap length. + * @param[in] dashPattern An array of alternating dash and gap lengths. * @param[in] cnt The length of the @p dashPattern array. - * @param[in] offset The shift of the starting point within the repeating dash pattern from which the path's dashing begins. + * @param[in] offset The shift of the starting point within the repeating dash pattern, from which the pattern begins to be applied. * - * @retval Result::InvalidArguments In case @p dashPattern is @c nullptr and @p cnt > 0, @p cnt is zero, any of the dash pattern values is zero or less. + * @retval Result::InvalidArguments In case @p dashPattern is @c nullptr and @p cnt > 0 or @p dashPattern is not @c nullptr and @p cnt is zero. * * @note To reset the stroke dash pattern, pass @c nullptr to @p dashPattern and zero to @p cnt. - * @warning @p cnt must be greater than 1 if the dash pattern is valid. + * @note Values of @p dashPattern less than zero are treated as zero. + * @note If all values in the @p dashPattern are equal to or less than 0, the dash is ignored. + * @note If the @p dashPattern contains an odd number of elements, the sequence is repeated in the same + * order to form an even-length pattern, preserving the alternation of dashes and gaps. * * @since 1.0 */ diff --git a/src/bindings/capi/thorvg_capi.h b/src/bindings/capi/thorvg_capi.h index 7ede30aa..3a0b7de8 100644 --- a/src/bindings/capi/thorvg_capi.h +++ b/src/bindings/capi/thorvg_capi.h @@ -1336,14 +1336,18 @@ TVG_API Tvg_Result tvg_shape_get_stroke_gradient(const Tvg_Paint* paint, Tvg_Gra * @brief Sets the shape's stroke dash pattern. * * @param[in] paint A Tvg_Paint pointer to the shape object. -* @param[in] dashPattern The array of consecutive pair values of the dash length and the gap length. +* @param[in] dashPattern An array of alternating dash and gap lengths. * @param[in] cnt The size of the @p dashPattern array. -* @param[in] offset The shift of the starting point within the repeating dash pattern from which the path's dashing begins. +* @param[in] offset The shift of the starting point within the repeating dash pattern, from which the pattern begins to be applied. * * @return Tvg_Result enumeration. -* @retval TVG_RESULT_INVALID_ARGUMENT An invalid pointer passed as an argument and @p cnt > 0, the given length of the array is less than two or any of the @p dashPattern values is zero or less. +* @retval TVG_RESULT_INVALID_ARGUMENT In case @p dashPattern is @c nullptr and @p cnt > 0 or @p dashPattern is not @c nullptr and @p cnt is zero. * * @note To reset the stroke dash pattern, pass @c nullptr to @p dashPattern and zero to @p cnt. +* @note Values of @p dashPattern less than zero are treated as zero. +* @note If all values in the @p dashPattern are equal to or less than 0, the dash is ignored. +* @note If the @p dashPattern contains an odd number of elements, the sequence is repeated in the same +* order to form an even-length pattern, preserving the alternation of dashes and gaps. * @since 1.0 */ TVG_API Tvg_Result tvg_shape_set_stroke_dash(Tvg_Paint* paint, const float* dashPattern, uint32_t cnt, float offset); diff --git a/src/loaders/lottie/tvgLottieBuilder.cpp b/src/loaders/lottie/tvgLottieBuilder.cpp index f0cada5c..56027481 100644 --- a/src/loaders/lottie/tvgLottieBuilder.cpp +++ b/src/loaders/lottie/tvgLottieBuilder.cpp @@ -219,15 +219,9 @@ static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext* ct ctx->propagator->strokeMiterlimit(stroke->miterLimit); if (stroke->dashattr) { - auto size = stroke->dashattr->size == 1 ? 2 : stroke->dashattr->size; - auto dashes = (float*)alloca(size * sizeof(float)); - for (uint8_t i = 0; i < stroke->dashattr->size; ++i) { - auto value = stroke->dashattr->values[i](frameNo, tween, exps); - //FIXME: allow the zero value in the engine level. - dashes[i] = value < FLT_EPSILON ? 0.01f : value; - } - if (stroke->dashattr->size == 1) dashes[1] = dashes[0]; - ctx->propagator->strokeDash(dashes, size, stroke->dashattr->offset(frameNo, tween, exps)); + auto dashes = (float*)alloca(stroke->dashattr->size * sizeof(float)); + for (uint8_t i = 0; i < stroke->dashattr->size; ++i) dashes[i] = stroke->dashattr->values[i](frameNo, tween, exps); + ctx->propagator->strokeDash(dashes, stroke->dashattr->size, stroke->dashattr->offset(frameNo, tween, exps)); } else { ctx->propagator->strokeDash(nullptr, 0); } diff --git a/src/loaders/svg/tvgSvgLoader.cpp b/src/loaders/svg/tvgSvgLoader.cpp index fef05b1c..952c9efd 100644 --- a/src/loaders/svg/tvgSvgLoader.cpp +++ b/src/loaders/svg/tvgSvgLoader.cpp @@ -338,18 +338,19 @@ static void _parseDashArray(SvgLoaderData* loader, const char *str, SvgDash* das str = _skipComma(str); auto parsedValue = toFloat(str, &end); if (str == end) break; - if (parsedValue <= 0.0f) break; + if (parsedValue < 0.0f) { + dash->array.reset(); + return; + } if (*end == '%') { ++end; //Refers to the diagonal length of the viewport. //https://www.w3.org/TR/SVG2/coords.html#Units parsedValue = (sqrtf(powf(loader->svgParse->global.w, 2) + powf(loader->svgParse->global.h, 2)) / sqrtf(2.0f)) * (parsedValue / 100.0f); } - (*dash).array.push(parsedValue); + dash->array.push(parsedValue); str = end; } - //If dash array size is 1, it means that dash and gap size are the same. - if ((*dash).array.count == 1) (*dash).array.push((*dash).array[0]); } diff --git a/src/renderer/gl_engine/tvgGlTessellator.cpp b/src/renderer/gl_engine/tvgGlTessellator.cpp index dc583261..8709091e 100644 --- a/src/renderer/gl_engine/tvgGlTessellator.cpp +++ b/src/renderer/gl_engine/tvgGlTessellator.cpp @@ -1509,7 +1509,7 @@ void Stroker::stroke(const RenderShape *rshape, const RenderPath& path) } auto& dash = rshape->stroke->dash; - if (dash.count == 0) doStroke(path); + if (dash.count == 0 || dash.length < DASH_PATTERN_THRESHOLD) doStroke(path); else doDashStroke(path, dash.pattern, dash.count, dash.offset, dash.length); } @@ -1578,7 +1578,7 @@ void Stroker::doDashStroke(const RenderPath& path, const float *patterns, uint32 dpath.pts.reserve(20 * path.pts.count); DashStroke dash(&dpath.cmds, &dpath.pts, patterns, patternCnt, offset, length); - dash.doStroke(path); + dash.doStroke(path, mStrokeCap != StrokeCap::Butt); doStroke(dpath); } @@ -1924,7 +1924,7 @@ DashStroke::DashStroke(Array *cmds, Array *pts, const float } -void DashStroke::doStroke(const RenderPath& path) +void DashStroke::doStroke(const RenderPath& path, bool drawPoint) { int32_t idx = 0; auto offset = mDashOffset; @@ -1947,7 +1947,7 @@ void DashStroke::doStroke(const RenderPath& path) ARRAY_FOREACH(cmd, path.cmds) { switch (*cmd) { case PathCommand::Close: { - this->dashLineTo(mPtStart); + this->dashLineTo(mPtStart, drawPoint); break; } case PathCommand::MoveTo: { @@ -1961,12 +1961,12 @@ void DashStroke::doStroke(const RenderPath& path) break; } case PathCommand::LineTo: { - this->dashLineTo(*pts); + this->dashLineTo(*pts, drawPoint); pts++; break; } case PathCommand::CubicTo: { - this->dashCubicTo(pts[0], pts[1], pts[2]); + this->dashCubicTo(pts[0], pts[1], pts[2], drawPoint); pts += 3; break; } @@ -1976,7 +1976,7 @@ void DashStroke::doStroke(const RenderPath& path) } -void DashStroke::dashLineTo(const Point& to) +void DashStroke::dashLineTo(const Point& to, bool drawPoint) { auto len = length(mPtCur - to); @@ -2007,7 +2007,18 @@ void DashStroke::dashLineTo(const Point& to) } this->lineTo(left.pt2); } - } else right = curr; + } else { + if (drawPoint && !mCurOpGap) { + if (drawPoint && !mCurOpGap) { + if (mMove || mDashPattern[mCurrIdx] < FLOAT_EPSILON) { + this->moveTo(curr.pt1); + mMove = false; + } + this->lineTo(curr.pt1); + } + } + right = curr; + } mCurrIdx = (mCurrIdx + 1) % mDashCount; mCurrLen = mDashPattern[mCurrIdx]; mCurOpGap = !mCurOpGap; @@ -2035,7 +2046,7 @@ void DashStroke::dashLineTo(const Point& to) } -void DashStroke::dashCubicTo(const Point& cnt1, const Point& cnt2, const Point& end) +void DashStroke::dashCubicTo(const Point& cnt1, const Point& cnt2, const Point& end, bool drawPoint) { Bezier cur; cur.start = {mPtCur.x, mPtCur.y}; @@ -2070,7 +2081,16 @@ void DashStroke::dashCubicTo(const Point& cnt1, const Point& cnt2, const Point& } this->cubicTo(left.ctrl1, left.ctrl2, left.end); } - } else right = cur; + } else { + if (drawPoint && !mCurOpGap) { + if (mMove || mDashPattern[mCurrIdx] < FLOAT_EPSILON) { + this->moveTo(cur.start); + mMove = false; + } + this->lineTo(cur.start); + } + right = cur; + } mCurrIdx = (mCurrIdx + 1) % mDashCount; mCurrLen = mDashPattern[mCurrIdx]; mCurOpGap = !mCurOpGap; diff --git a/src/renderer/gl_engine/tvgGlTessellator.h b/src/renderer/gl_engine/tvgGlTessellator.h index 44fbab41..50e64c9e 100644 --- a/src/renderer/gl_engine/tvgGlTessellator.h +++ b/src/renderer/gl_engine/tvgGlTessellator.h @@ -120,11 +120,11 @@ class DashStroke { public: DashStroke(Array* cmds, Array* pts, const float* patterns, uint32_t patternCnt, float offset, float length); - void doStroke(const RenderPath& path); + void doStroke(const RenderPath& path, bool drawPoint); private: - void dashLineTo(const Point& pt); - void dashCubicTo(const Point& pt1, const Point& pt2, const Point& pt3); + 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); diff --git a/src/renderer/sw_engine/tvgSwShape.cpp b/src/renderer/sw_engine/tvgSwShape.cpp index 71d9884a..c2c42f8e 100644 --- a/src/renderer/sw_engine/tvgSwShape.cpp +++ b/src/renderer/sw_engine/tvgSwShape.cpp @@ -97,7 +97,7 @@ static bool _outlineClose(SwOutline& outline) } -static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix& transform) +static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix& transform, bool drawPoint) { Line cur = {dash.ptCur, *to}; auto len = cur.length(); @@ -128,6 +128,13 @@ static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix& trans _outlineLineTo(*dash.outline, &left.pt2, transform); } } else { + if (drawPoint && !dash.curOpGap) { + if (dash.move || dash.pattern[dash.curIdx] < FLOAT_EPSILON) { + _outlineMoveTo(*dash.outline, &cur.pt1, transform); + dash.move = false; + } + _outlineLineTo(*dash.outline, &cur.pt1, transform); + } right = cur; } dash.curIdx = (dash.curIdx + 1) % dash.cnt; @@ -157,7 +164,7 @@ static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix& trans } -static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix& transform) +static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix& transform, bool drawPoint) { Bezier cur = {dash.ptCur, *ctrl1, *ctrl2, *to}; auto len = cur.length(); @@ -189,6 +196,13 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct _outlineCubicTo(*dash.outline, &left.ctrl1, &left.ctrl2, &left.end, transform); } } else { + if (drawPoint && !dash.curOpGap) { + if (dash.move || dash.pattern[dash.curIdx] < FLOAT_EPSILON) { + _outlineMoveTo(*dash.outline, &cur.start, transform); + dash.move = false; + } + _outlineLineTo(*dash.outline, &cur.start, transform); + } right = cur; } dash.curIdx = (dash.curIdx + 1) % dash.cnt; @@ -218,9 +232,9 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct } -static void _dashClose(SwDashStroke& dash, const Matrix& transform) +static void _dashClose(SwDashStroke& dash, const Matrix& transform, bool drawPoint) { - _dashLineTo(dash, &dash.ptStart, transform); + _dashLineTo(dash, &dash.ptStart, transform, drawPoint); } @@ -291,10 +305,11 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& trans pts++; } + auto drawPoint = rshape->stroke->cap != StrokeCap::Butt; while (--cmdCnt > 0) { switch (*cmds) { case PathCommand::Close: { - _dashClose(dash, transform); + _dashClose(dash, transform, drawPoint); break; } case PathCommand::MoveTo: { @@ -303,12 +318,12 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& trans break; } case PathCommand::LineTo: { - _dashLineTo(dash, pts, transform); + _dashLineTo(dash, pts, transform, drawPoint); ++pts; break; } case PathCommand::CubicTo: { - _dashCubicTo(dash, pts, pts + 1, pts + 2, transform); + _dashCubicTo(dash, pts, pts + 1, pts + 2, transform, drawPoint); pts += 3; break; } @@ -508,7 +523,17 @@ bool shapeGenStrokeRle(SwShape* shape, const RenderShape* rshape, const Matrix& auto ret = true; //Dash style with/without trimming - if (rshape->stroke->dash.count > 0) { + if (rshape->stroke->dash.count > 0 && rshape->stroke->dash.length > DASH_PATTERN_THRESHOLD) { + //check if the length of the drawn dashes is negligibly small => if yes, then it's a valid shape without a stroke + if (rshape->stroke->cap == StrokeCap::Butt) { + auto len = 0.0f; + for (uint32_t i = 0; i < rshape->stroke->dash.count; i += 2) len += rshape->stroke->dash.pattern[i]; + if (len < DASH_PATTERN_THRESHOLD) { + ret = true; + goto clear; + } + } + shapeOutline = _genDashOutline(rshape, transform, mpool, tid, rshape->trimpath()); if (!shapeOutline) return false; dashStroking = true; diff --git a/src/renderer/tvgShape.h b/src/renderer/tvgShape.h index 72d81454..6598b65a 100644 --- a/src/renderer/tvgShape.h +++ b/src/renderer/tvgShape.h @@ -290,7 +290,7 @@ struct ShapeImpl : Shape Result strokeDash(const float* pattern, uint32_t cnt, float offset) { - if ((cnt == 1) || (!pattern && cnt > 0) || (pattern && cnt == 0)) return Result::InvalidArguments; + if ((!pattern && cnt > 0) || (pattern && cnt == 0)) return Result::InvalidArguments; if (!rs.stroke) rs.stroke = new RenderStroke; //Reset dash auto& dash = rs.stroke->dash; @@ -302,11 +302,7 @@ struct ShapeImpl : Shape if (!dash.pattern) dash.pattern = tvg::malloc(sizeof(float) * cnt); dash.length = 0.0f; for (uint32_t i = 0; i < cnt; ++i) { - if (pattern[i] < DASH_PATTERN_THRESHOLD) { - dash.count = 0; - return Result::InvalidArguments; - } - dash.pattern[i] = pattern[i]; + dash.pattern[i] = pattern[i] < 0.0f ? 0.0f : pattern[i]; dash.length += dash.pattern[i]; } } diff --git a/src/renderer/wg_engine/tvgWgGeometry.h b/src/renderer/wg_engine/tvgWgGeometry.h index 906bf2dd..040ec332 100644 --- a/src/renderer/wg_engine/tvgWgGeometry.h +++ b/src/renderer/wg_engine/tvgWgGeometry.h @@ -359,7 +359,7 @@ struct WgIndexedVertexBuffer dashed->reset(scale); auto& dash = rstroke->dash; - if (buff.count < 2 || tvg::zero(dash.length)) return; + if (buff.count < 2) return; uint32_t index = 0; auto total = dash.pattern[index]; @@ -372,12 +372,14 @@ struct WgIndexedVertexBuffer auto gap = false; // scip dashes by offset - while(total <= dashOffset) { - index = (index + 1) % dash.count; - total += dash.pattern[index]; - gap = !gap; + if (dashOffset > 0.0f) { + while(total <= dashOffset) { + index = (index + 1) % dash.count; + total += dash.pattern[index]; + gap = !gap; + } + total -= dashOffset; } - total -= dashOffset; // iterate by polyline points for (uint32_t i = 0; i < buff.count - 1; i++) { @@ -466,7 +468,10 @@ struct WgIndexedVertexBuffer void appendSquare(Point v0, Point v1, float dist, float halfWidth) { - if(tvg::zero(dist)) return; + if(tvg::zero(dist)) { + appendQuad(v1 + Point{-halfWidth, -halfWidth}, v1 + Point{-halfWidth, halfWidth}, v1 + Point{halfWidth, -halfWidth}, v1 + Point{halfWidth, halfWidth}); + return; + } Point sub = v1 - v0; Point offset = sub / dist * halfWidth; Point nrm = {+offset.y, -offset.x}; diff --git a/src/renderer/wg_engine/tvgWgRenderData.cpp b/src/renderer/wg_engine/tvgWgRenderData.cpp index fba2a8a7..ef61c9bc 100644 --- a/src/renderer/wg_engine/tvgWgRenderData.cpp +++ b/src/renderer/wg_engine/tvgWgRenderData.cpp @@ -394,7 +394,7 @@ void WgRenderDataShape::proceedStrokes(WgContext& context, const RenderStroke* r { assert(rstroke); auto strokesGenerator = pool->reqIndexedVertexBuffer(buff.scale); - if (rstroke->dash.count == 0) strokesGenerator->appendStrokes(buff, rstroke); + if (rstroke->dash.count == 0 || rstroke->dash.length < DASH_PATTERN_THRESHOLD) strokesGenerator->appendStrokes(buff, rstroke); else strokesGenerator->appendStrokesDashed(buff, rstroke); appendStroke(context, *strokesGenerator); diff --git a/test/testShape.cpp b/test/testShape.cpp index 6361a1f7..20e84753 100644 --- a/test/testShape.cpp +++ b/test/testShape.cpp @@ -157,21 +157,30 @@ TEST_CASE("Stroking", "[tvgShape]") REQUIRE(shape->strokeFill(nullptr, nullptr, nullptr, nullptr) == Result::Success); //Stroke Dash - float dashPattern[3] = {0, 1.5f, 2.22f}; - REQUIRE(shape->strokeDash(dashPattern, 3) == Result::InvalidArguments); + REQUIRE(shape->strokeDash(nullptr, 3) == Result::InvalidArguments); - float dashPattern2[3] = {1.0f, 1.5f, 2.22f}; - REQUIRE(shape->strokeDash(dashPattern2, 3) == Result::Success); - REQUIRE(shape->strokeDash(dashPattern2, 3, 4.5) == Result::Success); + float dashPattern0[3] = {-10.0f, 1.5f, 2.22f}; + REQUIRE(shape->strokeDash(dashPattern0, 0) == Result::InvalidArguments); + REQUIRE(shape->strokeDash(dashPattern0, 3) == Result::Success); - const float* dashPattern3; + float dashPattern1[2] = {0.0f, 0.0f}; + REQUIRE(shape->strokeDash(dashPattern1, 2) == Result::Success); + + float dashPattern2[1] = {10.0f}; + REQUIRE(shape->strokeDash(dashPattern2, 1) == Result::Success); + + float dashPattern3[3] = {1.0f, 1.5f, 2.22f}; + REQUIRE(shape->strokeDash(dashPattern3, 3) == Result::Success); + REQUIRE(shape->strokeDash(dashPattern3, 3, 4.5) == Result::Success); + + const float* dashPattern4; float offset; REQUIRE(shape->strokeDash(nullptr) == 3); - REQUIRE(shape->strokeDash(&dashPattern3) == 3); - REQUIRE(shape->strokeDash(&dashPattern3, &offset) == 3); - REQUIRE(dashPattern3[0] == 1.0f); - REQUIRE(dashPattern3[1] == 1.5f); - REQUIRE(dashPattern3[2] == 2.22f); + REQUIRE(shape->strokeDash(&dashPattern4) == 3); + REQUIRE(shape->strokeDash(&dashPattern4, &offset) == 3); + REQUIRE(dashPattern4[0] == 1.0f); + REQUIRE(dashPattern4[1] == 1.5f); + REQUIRE(dashPattern4[2] == 2.22f); REQUIRE(offset == 4.5f); REQUIRE(shape->strokeDash(nullptr, 0) == Result::Success);