api: handling values <= 0 in strokeDash() api

The API allows now values <= 0 for dashes and gaps. Negative values
are treated as zero. The exception is when all provided values
are <= 0, in which case the dash is ignored.
This fixes the issue when dash = 0 was provided for strokes with round
or butt caps - the dot was not drawn, even though it should have been.

docs: the strokeDash API behavior's clarification for odd numbers
of values in dashPattern and refinement of the accepted values.
This commit is contained in:
Mira Grudzinska 2025-04-02 11:58:57 +02:00
parent 55847bdcb3
commit d71e11495d
11 changed files with 123 additions and 66 deletions

View file

@ -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
*/

View file

@ -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);

View file

@ -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);
}

View file

@ -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]);
}

View file

@ -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<PathCommand> *cmds, Array<Point> *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;

View file

@ -120,11 +120,11 @@ 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);
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);

View file

@ -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;

View file

@ -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<float*>(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];
}
}

View file

@ -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};

View file

@ -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);

View file

@ -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);