diff --git a/src/renderer/gl_engine/tvgGlTessellator.cpp b/src/renderer/gl_engine/tvgGlTessellator.cpp index b4968ea6..666763ff 100644 --- a/src/renderer/gl_engine/tvgGlTessellator.cpp +++ b/src/renderer/gl_engine/tvgGlTessellator.cpp @@ -1549,7 +1549,7 @@ void Stroker::stroke(const RenderShape *rshape) if (rshape->strokeTrim()) { auto begin = 0.0f; auto end = 0.0f; - rshape->stroke->strokeTrim(begin, end); + rshape->stroke->trim.get(begin, end); if (begin == end) return; diff --git a/src/renderer/meson.build b/src/renderer/meson.build index b00888a1..bfe09931 100644 --- a/src/renderer/meson.build +++ b/src/renderer/meson.build @@ -28,6 +28,7 @@ source_file = [ 'tvgShape.h', 'tvgTaskScheduler.h', 'tvgText.h', + 'tvgTrimPath.h', 'tvgAccessor.cpp', 'tvgAnimation.cpp', 'tvgCanvas.cpp', @@ -44,6 +45,7 @@ source_file = [ 'tvgSwCanvas.cpp', 'tvgTaskScheduler.cpp', 'tvgText.cpp', + 'tvgTrimPath.cpp', 'tvgWgCanvas.cpp' ] diff --git a/src/renderer/sw_engine/tvgSwShape.cpp b/src/renderer/sw_engine/tvgSwShape.cpp index 0ecd01ae..f1e27e6a 100644 --- a/src/renderer/sw_engine/tvgSwShape.cpp +++ b/src/renderer/sw_engine/tvgSwShape.cpp @@ -341,7 +341,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& trans dash.cnt = rshape->strokeDash((const float**)&dash.pattern, &offset); auto simultaneous = rshape->stroke->trim.simultaneous; float trimBegin = 0.0f, trimEnd = 1.0f; - if (trimmed) rshape->stroke->strokeTrim(trimBegin, trimEnd); + if (trimmed) rshape->stroke->trim.get(trimBegin, trimEnd); if (dash.cnt == 0) { if (trimmed) dash.pattern = (float*)malloc(sizeof(float) * 4); diff --git a/src/renderer/tvgRender.h b/src/renderer/tvgRender.h index ac45ea88..d5a1cdb0 100644 --- a/src/renderer/tvgRender.h +++ b/src/renderer/tvgRender.h @@ -28,6 +28,7 @@ #include "tvgCommon.h" #include "tvgArray.h" #include "tvgLock.h" +#include "tvgTrimPath.h" namespace tvg { @@ -102,16 +103,11 @@ struct RenderStroke uint32_t dashCnt = 0; float dashOffset = 0.0f; float miterlimit = 4.0f; + TrimPath trim; StrokeCap cap = StrokeCap::Square; StrokeJoin join = StrokeJoin::Bevel; bool strokeFirst = false; - struct { - float begin = 0.0f; - float end = 1.0f; - bool simultaneous = true; - } trim; - void operator=(const RenderStroke& rhs) { width = rhs.width; @@ -137,32 +133,6 @@ struct RenderStroke trim = rhs.trim; } - bool strokeTrim(float& begin, float& end) const - { - begin = trim.begin; - end = trim.end; - - if (fabsf(end - begin) >= 1.0f) { - begin = 0.0f; - end = 1.0f; - return false; - } - - auto loop = true; - - if (begin > 1.0f && end > 1.0f) loop = false; - if (begin < 0.0f && end < 0.0f) loop = false; - if (begin >= 0.0f && begin <= 1.0f && end >= 0.0f && end <= 1.0f) loop = false; - - if (begin > 1.0f) begin -= 1.0f; - if (begin < 0.0f) begin += 1.0f; - if (end > 1.0f) end -= 1.0f; - if (end < 0.0f) end += 1.0f; - - if ((loop && begin < end) || (!loop && begin > end)) std::swap(begin, end); - return true; - } - ~RenderStroke() { free(dashPattern); @@ -214,9 +184,7 @@ struct RenderShape bool strokeTrim() const { if (!stroke) return false; - if (stroke->trim.begin == 0.0f && stroke->trim.end == 1.0f) return false; - if (fabsf(stroke->trim.end - stroke->trim.begin) >= 1.0f) return false; - return true; + return stroke->trim.valid(); } bool strokeFill(uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a) const diff --git a/src/renderer/tvgShape.h b/src/renderer/tvgShape.h index caa058f6..3ed2aca7 100644 --- a/src/renderer/tvgShape.h +++ b/src/renderer/tvgShape.h @@ -211,6 +211,11 @@ struct Shape::Impl : Paint::Impl void strokeTrim(float begin, float end, bool simultaneous) { + if (fabsf(end - begin) >= 1.0f) { + begin = 0.0f; + end = 1.0f; + } + if (!rs.stroke) { if (begin == 0.0f && end == 1.0f) return; rs.stroke = new RenderStroke(); diff --git a/src/renderer/tvgTrimPath.cpp b/src/renderer/tvgTrimPath.cpp new file mode 100644 index 00000000..a9f57ac6 --- /dev/null +++ b/src/renderer/tvgTrimPath.cpp @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2025 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +#include "tvgTrimPath.h" +#include "tvgMath.h" +#include "tvgRender.h" + +#define EPSILON 1e-4f + +/************************************************************************/ +/* Internal Class Implementation */ +/************************************************************************/ + +static float _pathLength(const PathCommand* cmds, uint32_t cmdsCnt, const Point* pts, uint32_t ptsCnt) +{ + if (ptsCnt < 2) return 0.0f; + + auto start = pts; + auto totalLength = 0.0f; + + while (cmdsCnt-- > 0) { + switch (*cmds) { + case PathCommand::Close: { + totalLength += length(pts - 1, start); + break; + } + case PathCommand::MoveTo: { + start = pts; + ++pts; + break; + } + case PathCommand::LineTo: { + totalLength += length(pts - 1, pts); + ++pts; + break; + } + case PathCommand::CubicTo: { + totalLength += Bezier{*(pts - 1), *pts, *(pts + 1), *(pts + 2)}.length(); + pts += 3; + break; + } + } + ++cmds; + } + + return totalLength; +} + + +static void _trimAt(const PathCommand* cmds, const Point* pts, Point& moveTo, float at1, float at2, bool start, RenderPath& out) +{ + switch (*cmds) { + case PathCommand::MoveTo: + break; + case PathCommand::LineTo: { + Line tmp, left, right; + Line{*(pts - 1), *pts}.split(at1, left, tmp); + tmp.split(at2, left, right); + if (start) { + out.pts.push(left.pt1); + moveTo = left.pt1; + out.cmds.push(PathCommand::MoveTo); + } + out.pts.push(left.pt2); + out.cmds.push(PathCommand::LineTo); + break; + } + case PathCommand::CubicTo: { + Bezier tmp, left, right; + Bezier{*(pts - 1), *pts, *(pts + 1), *(pts + 2)}.split(at1, left, tmp); + tmp.split(at2, left, right); + if (start) { + moveTo = left.start; + out.pts.push(left.start); + out.cmds.push(PathCommand::MoveTo); + } + out.pts.push(left.ctrl1); + out.pts.push(left.ctrl2); + out.pts.push(left.end); + out.cmds.push(PathCommand::CubicTo); + break; + } + case PathCommand::Close: { + Line tmp, left, right; + Line{*(pts - 1), moveTo}.split(at1, left, tmp); + tmp.split(at2, left, right); + if (start) { + moveTo = left.pt1; + out.pts.push(left.pt1); + out.cmds.push(PathCommand::MoveTo); + } + out.pts.push(left.pt2); + out.cmds.push(PathCommand::LineTo); + break; + } + } +} + + +static void _add(const PathCommand* cmds, const Point* pts, const Point& moveTo, bool& start, RenderPath& out) +{ + switch (*cmds) { + case PathCommand::MoveTo: { + out.cmds.push(PathCommand::MoveTo); + out.pts.push(*pts); + start = false; + break; + } + case PathCommand::LineTo: { + if (start) { + out.cmds.push(PathCommand::MoveTo); + out.pts.push(*(pts - 1)); + } + out.cmds.push(PathCommand::LineTo); + out.pts.push(*pts); + start = false; + break; + } + case PathCommand::CubicTo: { + if (start) { + out.cmds.push(PathCommand::MoveTo); + out.pts.push(*(pts - 1)); + } + out.cmds.push(PathCommand::CubicTo); + out.pts.push(*pts); + out.pts.push(*(pts + 1)); + out.pts.push(*(pts + 2)); + start = false; + break; + } + case PathCommand::Close: { + if (start) { + out.cmds.push(PathCommand::MoveTo); + out.pts.push(*(pts - 1)); + } + out.cmds.push(PathCommand::LineTo); + out.pts.push(moveTo); + start = true; + break; + } + } +} + + +static void _trimPath(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, TVG_UNUSED uint32_t inPtsCnt, float trimStart, float trimEnd, RenderPath& out) +{ + auto cmds = const_cast(inCmds); + auto pts = const_cast(inPts); + auto moveToTrimmed = *pts; + auto moveTo = *pts; + auto len = 0.0f; + + auto _length = [&]() -> float { + switch (*cmds) { + case PathCommand::MoveTo: + return 0.0f; + case PathCommand::LineTo: + return length(pts - 1, pts); + case PathCommand::CubicTo: + return Bezier{*(pts - 1), *pts, *(pts + 1), *(pts + 2)}.length(); + case PathCommand::Close: + return length(pts - 1, &moveTo); + } + return 0.0f; + }; + + auto _shift = [&]() -> void { + switch (*cmds) { + case PathCommand::MoveTo: + moveTo = *pts; + moveToTrimmed = *pts; + ++pts; + break; + case PathCommand::LineTo: + ++pts; + break; + case PathCommand::CubicTo: + pts += 3; + break; + case PathCommand::Close: + break; + } + ++cmds; + }; + + bool start = true; + for (uint32_t i = 0; i < inCmdsCnt; ++i) { + auto dLen = _length(); + + //very short segments are skipped since due to the finite precision of Bezier curve subdivision and length calculation (1e-2), + //trimming may produce very short segments that would effectively have zero length with higher computational accuracy. + if (len <= trimStart) { + //cut the segment at the beginning and at the end + if (len + dLen > trimEnd) { + _trimAt(cmds, pts, moveToTrimmed, trimStart - len, trimEnd - trimStart, start, out); + start = false; + //cut the segment at the beginning + } else if (len + dLen > trimStart + EPSILON) { + _trimAt(cmds, pts, moveToTrimmed, trimStart - len, len + dLen - trimStart, start, out); + start = false; + } + } else if (len <= trimEnd - EPSILON) { + //cut the segment at the end + if (len + dLen > trimEnd) { + _trimAt(cmds, pts, moveTo, 0.0f, trimEnd - len, start, out); + start = true; + //add the whole segment + } else _add(cmds, pts, moveTo, start, out); + } + + len += dLen; + _shift(); + } +} + + +static void _trim(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, float begin, float end, RenderPath& out) +{ + auto totalLength = _pathLength(inCmds, inCmdsCnt, inPts, inPtsCnt); + auto trimStart = begin * totalLength; + auto trimEnd = end * totalLength; + + if (trimStart > trimEnd) { + _trimPath(inCmds, inCmdsCnt, inPts, inPtsCnt, trimStart, totalLength, out); + _trimPath(inCmds, inCmdsCnt, inPts, inPtsCnt, 0.0f, trimEnd, out); + } else { + _trimPath(inCmds, inCmdsCnt, inPts, inPtsCnt, trimStart, trimEnd, out); + } +} + + +/************************************************************************/ +/* External Class Implementation */ +/************************************************************************/ + + +bool TrimPath::valid() const +{ + if (begin == 0.0f && end == 1.0f) return false; + return true; +} + + +bool TrimPath::get(float& begin, float& end) const +{ + begin = this->begin; + end = this->end; + + auto loop = true; + + if (begin > 1.0f && end > 1.0f) loop = false; + if (begin < 0.0f && end < 0.0f) loop = false; + if (begin >= 0.0f && begin <= 1.0f && end >= 0.0f && end <= 1.0f) loop = false; + + if (begin > 1.0f) begin -= 1.0f; + if (begin < 0.0f) begin += 1.0f; + if (end > 1.0f) end -= 1.0f; + if (end < 0.0f) end += 1.0f; + + if ((loop && begin < end) || (!loop && begin > end)) std::swap(begin, end); + return true; +} + + +bool TrimPath::trim(const RenderPath& in, RenderPath& out) const +{ + float begin, end; + get(begin, end); + + if (in.pts.count < 2 || tvg::zero(begin - end)) return false; + + out.cmds.reserve(in.cmds.count * 2); + out.pts.reserve(in.pts.count * 2); + + auto pts = in.pts.data; + auto cmds = in.cmds.data; + + if (simultaneous) { + auto startCmds = cmds; + auto startPts = pts; + uint32_t i = 0; + while (i < in.cmds.count) { + switch (in.cmds[i]) { + case PathCommand::MoveTo: { + if (startCmds != cmds) _trim(startCmds, cmds - startCmds, startPts, pts - startPts, begin, end, out); + startPts = pts; + startCmds = cmds; + ++pts; + ++cmds; + break; + } + case PathCommand::LineTo: { + ++pts; + ++cmds; + break; + } + case PathCommand::CubicTo: { + pts += 3; + ++cmds; + break; + } + case PathCommand::Close: { + ++cmds; + if (startCmds != cmds) _trim(startCmds, cmds - startCmds, startPts, pts - startPts, begin, end, out); + startPts = pts; + startCmds = cmds; + break; + } + } + i++; + } + if (startCmds != cmds) _trim(startCmds, cmds - startCmds, startPts, pts - startPts, begin, end, out); + } else { + _trim(in.cmds.data, in.cmds.count, in.pts.data, in.pts.count, begin, end, out); + } + + return out.pts.count >= 2; +} diff --git a/src/renderer/tvgTrimPath.h b/src/renderer/tvgTrimPath.h new file mode 100644 index 00000000..d2638fe3 --- /dev/null +++ b/src/renderer/tvgTrimPath.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef _TVG_TRIM_PATH_H +#define _TVG_TRIM_PATH_H + +namespace tvg +{ +struct RenderPath; + +struct TrimPath +{ + float begin = 0.0f; + float end = 1.0f; + bool simultaneous = true; + + bool valid() const; + bool get(float& begin, float& end) const; + bool trim(const RenderPath& in, RenderPath& out) const; +}; + +} + +#endif //_TVG_TRIM_PATH_H diff --git a/src/renderer/wg_engine/tvgWgRenderData.cpp b/src/renderer/wg_engine/tvgWgRenderData.cpp index 918729fb..e8c7f696 100755 --- a/src/renderer/wg_engine/tvgWgRenderData.cpp +++ b/src/renderer/wg_engine/tvgWgRenderData.cpp @@ -393,7 +393,7 @@ void WgRenderDataShape::updateMeshes(WgContext& context, const RenderShape &rsha // append shape with strokes } else { float tbeg{}, tend{}; - if (!rshape.stroke->strokeTrim(tbeg, tend)) { tbeg = 0.0f; tend = 1.0f; } + if (!rshape.stroke->trim.get(tbeg, tend)) { tbeg = 0.0f; tend = 1.0f; } bool loop = tbeg > tend; if (tbeg == tend) { pbuff.decodePath(rshape, false, [&](const WgVertexBuffer& path_buff) {