common: introduce TrimPath struct

The first step towards unifying trimming across engines.

@Issue: https://github.com/thorvg/thorvg/issues/2854
This commit is contained in:
Mira Grudzinska 2025-01-14 22:48:34 +01:00 committed by Hermet Park
parent 4f0f2e4c08
commit 669d6dc580
8 changed files with 394 additions and 38 deletions

View file

@ -1549,7 +1549,7 @@ void Stroker::stroke(const RenderShape *rshape)
if (rshape->strokeTrim()) { if (rshape->strokeTrim()) {
auto begin = 0.0f; auto begin = 0.0f;
auto end = 0.0f; auto end = 0.0f;
rshape->stroke->strokeTrim(begin, end); rshape->stroke->trim.get(begin, end);
if (begin == end) return; if (begin == end) return;

View file

@ -28,6 +28,7 @@ source_file = [
'tvgShape.h', 'tvgShape.h',
'tvgTaskScheduler.h', 'tvgTaskScheduler.h',
'tvgText.h', 'tvgText.h',
'tvgTrimPath.h',
'tvgAccessor.cpp', 'tvgAccessor.cpp',
'tvgAnimation.cpp', 'tvgAnimation.cpp',
'tvgCanvas.cpp', 'tvgCanvas.cpp',
@ -44,6 +45,7 @@ source_file = [
'tvgSwCanvas.cpp', 'tvgSwCanvas.cpp',
'tvgTaskScheduler.cpp', 'tvgTaskScheduler.cpp',
'tvgText.cpp', 'tvgText.cpp',
'tvgTrimPath.cpp',
'tvgWgCanvas.cpp' 'tvgWgCanvas.cpp'
] ]

View file

@ -341,7 +341,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& trans
dash.cnt = rshape->strokeDash((const float**)&dash.pattern, &offset); dash.cnt = rshape->strokeDash((const float**)&dash.pattern, &offset);
auto simultaneous = rshape->stroke->trim.simultaneous; auto simultaneous = rshape->stroke->trim.simultaneous;
float trimBegin = 0.0f, trimEnd = 1.0f; 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 (dash.cnt == 0) {
if (trimmed) dash.pattern = (float*)malloc(sizeof(float) * 4); if (trimmed) dash.pattern = (float*)malloc(sizeof(float) * 4);

View file

@ -28,6 +28,7 @@
#include "tvgCommon.h" #include "tvgCommon.h"
#include "tvgArray.h" #include "tvgArray.h"
#include "tvgLock.h" #include "tvgLock.h"
#include "tvgTrimPath.h"
namespace tvg namespace tvg
{ {
@ -102,16 +103,11 @@ struct RenderStroke
uint32_t dashCnt = 0; uint32_t dashCnt = 0;
float dashOffset = 0.0f; float dashOffset = 0.0f;
float miterlimit = 4.0f; float miterlimit = 4.0f;
TrimPath trim;
StrokeCap cap = StrokeCap::Square; StrokeCap cap = StrokeCap::Square;
StrokeJoin join = StrokeJoin::Bevel; StrokeJoin join = StrokeJoin::Bevel;
bool strokeFirst = false; bool strokeFirst = false;
struct {
float begin = 0.0f;
float end = 1.0f;
bool simultaneous = true;
} trim;
void operator=(const RenderStroke& rhs) void operator=(const RenderStroke& rhs)
{ {
width = rhs.width; width = rhs.width;
@ -137,32 +133,6 @@ struct RenderStroke
trim = rhs.trim; 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() ~RenderStroke()
{ {
free(dashPattern); free(dashPattern);
@ -214,9 +184,7 @@ struct RenderShape
bool strokeTrim() const bool strokeTrim() const
{ {
if (!stroke) return false; if (!stroke) return false;
if (stroke->trim.begin == 0.0f && stroke->trim.end == 1.0f) return false; return stroke->trim.valid();
if (fabsf(stroke->trim.end - stroke->trim.begin) >= 1.0f) return false;
return true;
} }
bool strokeFill(uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a) const bool strokeFill(uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a) const

View file

@ -211,6 +211,11 @@ struct Shape::Impl : Paint::Impl
void strokeTrim(float begin, float end, bool simultaneous) void strokeTrim(float begin, float end, bool simultaneous)
{ {
if (fabsf(end - begin) >= 1.0f) {
begin = 0.0f;
end = 1.0f;
}
if (!rs.stroke) { if (!rs.stroke) {
if (begin == 0.0f && end == 1.0f) return; if (begin == 0.0f && end == 1.0f) return;
rs.stroke = new RenderStroke(); rs.stroke = new RenderStroke();

View file

@ -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<PathCommand*>(inCmds);
auto pts = const_cast<Point*>(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;
}

View file

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

View file

@ -393,7 +393,7 @@ void WgRenderDataShape::updateMeshes(WgContext& context, const RenderShape &rsha
// append shape with strokes // append shape with strokes
} else { } else {
float tbeg{}, tend{}; 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; bool loop = tbeg > tend;
if (tbeg == tend) { if (tbeg == tend) {
pbuff.decodePath(rshape, false, [&](const WgVertexBuffer& path_buff) { pbuff.decodePath(rshape, false, [&](const WgVertexBuffer& path_buff) {