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. * @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] 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. * @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 * @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. * @brief Sets the shape's stroke dash pattern.
* *
* @param[in] paint A Tvg_Paint pointer to the shape object. * @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] 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. * @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 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 * @since 1.0
*/ */
TVG_API Tvg_Result tvg_shape_set_stroke_dash(Tvg_Paint* paint, const float* dashPattern, uint32_t cnt, float offset); 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); ctx->propagator->strokeMiterlimit(stroke->miterLimit);
if (stroke->dashattr) { if (stroke->dashattr) {
auto size = stroke->dashattr->size == 1 ? 2 : stroke->dashattr->size; auto dashes = (float*)alloca(stroke->dashattr->size * sizeof(float));
auto dashes = (float*)alloca(size * sizeof(float)); for (uint8_t i = 0; i < stroke->dashattr->size; ++i) dashes[i] = stroke->dashattr->values[i](frameNo, tween, exps);
for (uint8_t i = 0; i < stroke->dashattr->size; ++i) { ctx->propagator->strokeDash(dashes, stroke->dashattr->size, stroke->dashattr->offset(frameNo, tween, exps));
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));
} else { } else {
ctx->propagator->strokeDash(nullptr, 0); ctx->propagator->strokeDash(nullptr, 0);
} }

View file

@ -338,18 +338,19 @@ static void _parseDashArray(SvgLoaderData* loader, const char *str, SvgDash* das
str = _skipComma(str); str = _skipComma(str);
auto parsedValue = toFloat(str, &end); auto parsedValue = toFloat(str, &end);
if (str == end) break; if (str == end) break;
if (parsedValue <= 0.0f) break; if (parsedValue < 0.0f) {
dash->array.reset();
return;
}
if (*end == '%') { if (*end == '%') {
++end; ++end;
//Refers to the diagonal length of the viewport. //Refers to the diagonal length of the viewport.
//https://www.w3.org/TR/SVG2/coords.html#Units //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); 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; 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; 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); 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); dpath.pts.reserve(20 * path.pts.count);
DashStroke dash(&dpath.cmds, &dpath.pts, patterns, patternCnt, offset, length); DashStroke dash(&dpath.cmds, &dpath.pts, patterns, patternCnt, offset, length);
dash.doStroke(path); dash.doStroke(path, mStrokeCap != StrokeCap::Butt);
doStroke(dpath); 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; int32_t idx = 0;
auto offset = mDashOffset; auto offset = mDashOffset;
@ -1947,7 +1947,7 @@ void DashStroke::doStroke(const RenderPath& path)
ARRAY_FOREACH(cmd, path.cmds) { ARRAY_FOREACH(cmd, path.cmds) {
switch (*cmd) { switch (*cmd) {
case PathCommand::Close: { case PathCommand::Close: {
this->dashLineTo(mPtStart); this->dashLineTo(mPtStart, drawPoint);
break; break;
} }
case PathCommand::MoveTo: { case PathCommand::MoveTo: {
@ -1961,12 +1961,12 @@ void DashStroke::doStroke(const RenderPath& path)
break; break;
} }
case PathCommand::LineTo: { case PathCommand::LineTo: {
this->dashLineTo(*pts); this->dashLineTo(*pts, drawPoint);
pts++; pts++;
break; break;
} }
case PathCommand::CubicTo: { case PathCommand::CubicTo: {
this->dashCubicTo(pts[0], pts[1], pts[2]); this->dashCubicTo(pts[0], pts[1], pts[2], drawPoint);
pts += 3; pts += 3;
break; 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); auto len = length(mPtCur - to);
@ -2007,7 +2007,18 @@ void DashStroke::dashLineTo(const Point& to)
} }
this->lineTo(left.pt2); 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; mCurrIdx = (mCurrIdx + 1) % mDashCount;
mCurrLen = mDashPattern[mCurrIdx]; mCurrLen = mDashPattern[mCurrIdx];
mCurOpGap = !mCurOpGap; 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; Bezier cur;
cur.start = {mPtCur.x, mPtCur.y}; 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); 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; mCurrIdx = (mCurrIdx + 1) % mDashCount;
mCurrLen = mDashPattern[mCurrIdx]; mCurrLen = mDashPattern[mCurrIdx];
mCurOpGap = !mCurOpGap; mCurOpGap = !mCurOpGap;

View file

@ -120,11 +120,11 @@ class DashStroke
{ {
public: public:
DashStroke(Array<PathCommand>* cmds, Array<Point>* pts, const float* patterns, uint32_t patternCnt, float offset, float length); 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: private:
void dashLineTo(const Point& pt); void dashLineTo(const Point& pt, bool drawPoint);
void dashCubicTo(const Point& pt1, const Point& pt2, const Point& pt3); void dashCubicTo(const Point& pt1, const Point& pt2, const Point& pt3, bool drawPoint);
void moveTo(const Point& pt); void moveTo(const Point& pt);
void lineTo(const Point& pt); void lineTo(const Point& pt);
void cubicTo(const Point& pt1, const Point& pt2, const Point& pt3); 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}; Line cur = {dash.ptCur, *to};
auto len = cur.length(); 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); _outlineLineTo(*dash.outline, &left.pt2, transform);
} }
} else { } 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; right = cur;
} }
dash.curIdx = (dash.curIdx + 1) % dash.cnt; 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}; Bezier cur = {dash.ptCur, *ctrl1, *ctrl2, *to};
auto len = cur.length(); 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); _outlineCubicTo(*dash.outline, &left.ctrl1, &left.ctrl2, &left.end, transform);
} }
} else { } 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; right = cur;
} }
dash.curIdx = (dash.curIdx + 1) % dash.cnt; 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++; pts++;
} }
auto drawPoint = rshape->stroke->cap != StrokeCap::Butt;
while (--cmdCnt > 0) { while (--cmdCnt > 0) {
switch (*cmds) { switch (*cmds) {
case PathCommand::Close: { case PathCommand::Close: {
_dashClose(dash, transform); _dashClose(dash, transform, drawPoint);
break; break;
} }
case PathCommand::MoveTo: { case PathCommand::MoveTo: {
@ -303,12 +318,12 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& trans
break; break;
} }
case PathCommand::LineTo: { case PathCommand::LineTo: {
_dashLineTo(dash, pts, transform); _dashLineTo(dash, pts, transform, drawPoint);
++pts; ++pts;
break; break;
} }
case PathCommand::CubicTo: { case PathCommand::CubicTo: {
_dashCubicTo(dash, pts, pts + 1, pts + 2, transform); _dashCubicTo(dash, pts, pts + 1, pts + 2, transform, drawPoint);
pts += 3; pts += 3;
break; break;
} }
@ -508,7 +523,17 @@ bool shapeGenStrokeRle(SwShape* shape, const RenderShape* rshape, const Matrix&
auto ret = true; auto ret = true;
//Dash style with/without trimming //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()); shapeOutline = _genDashOutline(rshape, transform, mpool, tid, rshape->trimpath());
if (!shapeOutline) return false; if (!shapeOutline) return false;
dashStroking = true; dashStroking = true;

View file

@ -290,7 +290,7 @@ struct ShapeImpl : Shape
Result strokeDash(const float* pattern, uint32_t cnt, float offset) 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; if (!rs.stroke) rs.stroke = new RenderStroke;
//Reset dash //Reset dash
auto& dash = rs.stroke->dash; auto& dash = rs.stroke->dash;
@ -302,11 +302,7 @@ struct ShapeImpl : Shape
if (!dash.pattern) dash.pattern = tvg::malloc<float*>(sizeof(float) * cnt); if (!dash.pattern) dash.pattern = tvg::malloc<float*>(sizeof(float) * cnt);
dash.length = 0.0f; dash.length = 0.0f;
for (uint32_t i = 0; i < cnt; ++i) { for (uint32_t i = 0; i < cnt; ++i) {
if (pattern[i] < DASH_PATTERN_THRESHOLD) { dash.pattern[i] = pattern[i] < 0.0f ? 0.0f : pattern[i];
dash.count = 0;
return Result::InvalidArguments;
}
dash.pattern[i] = pattern[i];
dash.length += dash.pattern[i]; dash.length += dash.pattern[i];
} }
} }

View file

@ -359,7 +359,7 @@ struct WgIndexedVertexBuffer
dashed->reset(scale); dashed->reset(scale);
auto& dash = rstroke->dash; auto& dash = rstroke->dash;
if (buff.count < 2 || tvg::zero(dash.length)) return; if (buff.count < 2) return;
uint32_t index = 0; uint32_t index = 0;
auto total = dash.pattern[index]; auto total = dash.pattern[index];
@ -372,12 +372,14 @@ struct WgIndexedVertexBuffer
auto gap = false; auto gap = false;
// scip dashes by offset // scip dashes by offset
if (dashOffset > 0.0f) {
while(total <= dashOffset) { while(total <= dashOffset) {
index = (index + 1) % dash.count; index = (index + 1) % dash.count;
total += dash.pattern[index]; total += dash.pattern[index];
gap = !gap; gap = !gap;
} }
total -= dashOffset; total -= dashOffset;
}
// iterate by polyline points // iterate by polyline points
for (uint32_t i = 0; i < buff.count - 1; i++) { 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) 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 sub = v1 - v0;
Point offset = sub / dist * halfWidth; Point offset = sub / dist * halfWidth;
Point nrm = {+offset.y, -offset.x}; Point nrm = {+offset.y, -offset.x};

View file

@ -394,7 +394,7 @@ void WgRenderDataShape::proceedStrokes(WgContext& context, const RenderStroke* r
{ {
assert(rstroke); assert(rstroke);
auto strokesGenerator = pool->reqIndexedVertexBuffer(buff.scale); 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); else strokesGenerator->appendStrokesDashed(buff, rstroke);
appendStroke(context, *strokesGenerator); appendStroke(context, *strokesGenerator);

View file

@ -157,21 +157,30 @@ TEST_CASE("Stroking", "[tvgShape]")
REQUIRE(shape->strokeFill(nullptr, nullptr, nullptr, nullptr) == Result::Success); REQUIRE(shape->strokeFill(nullptr, nullptr, nullptr, nullptr) == Result::Success);
//Stroke Dash //Stroke Dash
float dashPattern[3] = {0, 1.5f, 2.22f}; REQUIRE(shape->strokeDash(nullptr, 3) == Result::InvalidArguments);
REQUIRE(shape->strokeDash(dashPattern, 3) == Result::InvalidArguments);
float dashPattern2[3] = {1.0f, 1.5f, 2.22f}; float dashPattern0[3] = {-10.0f, 1.5f, 2.22f};
REQUIRE(shape->strokeDash(dashPattern2, 3) == Result::Success); REQUIRE(shape->strokeDash(dashPattern0, 0) == Result::InvalidArguments);
REQUIRE(shape->strokeDash(dashPattern2, 3, 4.5) == Result::Success); 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; float offset;
REQUIRE(shape->strokeDash(nullptr) == 3); REQUIRE(shape->strokeDash(nullptr) == 3);
REQUIRE(shape->strokeDash(&dashPattern3) == 3); REQUIRE(shape->strokeDash(&dashPattern4) == 3);
REQUIRE(shape->strokeDash(&dashPattern3, &offset) == 3); REQUIRE(shape->strokeDash(&dashPattern4, &offset) == 3);
REQUIRE(dashPattern3[0] == 1.0f); REQUIRE(dashPattern4[0] == 1.0f);
REQUIRE(dashPattern3[1] == 1.5f); REQUIRE(dashPattern4[1] == 1.5f);
REQUIRE(dashPattern3[2] == 2.22f); REQUIRE(dashPattern4[2] == 2.22f);
REQUIRE(offset == 4.5f); REQUIRE(offset == 4.5f);
REQUIRE(shape->strokeDash(nullptr, 0) == Result::Success); REQUIRE(shape->strokeDash(nullptr, 0) == Result::Success);