mirror of
https://github.com/thorvg/thorvg.git
synced 2025-06-13 19:44:28 +00:00

On a close path command creates a new object started form the closed point issue: https://github.com/thorvg/thorvg/pull/2923
450 lines
No EOL
16 KiB
C++
Executable file
450 lines
No EOL
16 KiB
C++
Executable file
/*
|
|
* Copyright (c) 2023 - 2024 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_WG_GEOMETRY_H_
|
|
#define _TVG_WG_GEOMETRY_H_
|
|
|
|
#include <cassert>
|
|
#include "tvgMath.h"
|
|
#include "tvgRender.h"
|
|
|
|
|
|
// default size of vertex and index buffers
|
|
#define WG_POINTS_COUNT 32768
|
|
|
|
// simple vertex buffer
|
|
struct WgVertexBuffer
|
|
{
|
|
Point vbuff[WG_POINTS_COUNT]; // vertex buffer
|
|
float vdist[WG_POINTS_COUNT]; // distance to previous point
|
|
float vleng[WG_POINTS_COUNT]; // distance to the first point through all previous points
|
|
size_t vcount{};
|
|
bool closed{};
|
|
float tscale = 1.0f; // tesselation scale
|
|
|
|
// callback for external process of polyline
|
|
using onPolylineFn = std::function<void(const WgVertexBuffer& buff)>;
|
|
|
|
// reset buffer
|
|
void reset(float scale)
|
|
{
|
|
vcount = 0;
|
|
closed = false;
|
|
tscale = scale;
|
|
}
|
|
|
|
// get the last point with optional index offset from the end
|
|
Point last(size_t offset = 0) const
|
|
{
|
|
return vbuff[vcount - offset - 1];
|
|
}
|
|
|
|
// get the last distance with optional index offset from the end
|
|
float lastDist(size_t offset = 0) const
|
|
{
|
|
return vdist[vcount - offset - 1];
|
|
}
|
|
|
|
// get total length
|
|
float total() const
|
|
{
|
|
return (vcount == 0) ? 0.0f : vleng[vcount-1];
|
|
}
|
|
|
|
// get next vertex index by length using binary search
|
|
size_t getIndexByLength(float len) const
|
|
{
|
|
if (vcount <= 1) return 0;
|
|
size_t left = 0;
|
|
size_t right = vcount - 1;
|
|
while (left <= right) {
|
|
size_t mid = left + (right - left) / 2;
|
|
if (vleng[mid] == len) return mid;
|
|
else if (vleng[mid] < len) left = mid + 1;
|
|
else right = mid - 1;
|
|
}
|
|
return right + 1;
|
|
}
|
|
|
|
// get min and max values of the buffer
|
|
void getMinMax(Point& pmin, Point& pmax) const
|
|
{
|
|
if (vcount == 0) return;
|
|
pmax = pmin = vbuff[0];
|
|
for (size_t i = 1; i < vcount; i++) {
|
|
pmin = min(pmin, vbuff[i]);
|
|
pmax = max(pmax, vbuff[i]);
|
|
}
|
|
}
|
|
|
|
// update points distancess to the prev point and total length
|
|
void updateDistances()
|
|
{
|
|
if (vcount == 0) return;
|
|
vdist[0] = 0.0f;
|
|
vleng[0] = 0.0f;
|
|
for (size_t i = 1; i < vcount; i++) {
|
|
vdist[i] = length(vbuff[i-1] - vbuff[i]);
|
|
vleng[i] = vleng[i-1] + vdist[i];
|
|
}
|
|
}
|
|
|
|
// close vertex buffer
|
|
void close()
|
|
{
|
|
// check if last point is not to close to the first point
|
|
if (!tvg::zero(length2(vbuff[0] - last())))
|
|
append(vbuff[0]);
|
|
closed = true;
|
|
}
|
|
|
|
// append point
|
|
void append(const Point& p)
|
|
{
|
|
vbuff[vcount] = p;
|
|
vcount++;
|
|
}
|
|
|
|
// append source vertex buffer in index range from start to end (end not included)
|
|
void appendRange(const WgVertexBuffer& buff, size_t start_index, size_t end_index)
|
|
{
|
|
for (size_t i = start_index; i < end_index; i++)
|
|
append(buff.vbuff[i]);
|
|
}
|
|
|
|
// append circle (list of triangles)
|
|
void appendCircle(float radius)
|
|
{
|
|
// get approx circle length
|
|
float clen = 2.0f * radius * MATH_PI;
|
|
size_t nsegs = std::max((uint32_t)(clen * tscale / 8), 16U);
|
|
// append circle^
|
|
Point prev { std::sin(0.0f) * radius, std::cos(0.0f) * radius };
|
|
for (size_t i = 1; i <= nsegs; i++) {
|
|
float t = (2.0f * MATH_PI * i) / nsegs;
|
|
Point curr { std::sin(t) * radius, std::cos(t) * radius };
|
|
append(Point{0.0f, 0.0f});
|
|
append(prev);
|
|
append(curr);
|
|
prev = curr;
|
|
}
|
|
}
|
|
|
|
// append cubic spline
|
|
void appendCubic(const Point& v0, const Point& v1, const Point& v2, const Point& v3)
|
|
{
|
|
// get approx cubic length
|
|
float clen = (length(v0 - v1) + length(v1 - v2) + length(v2 - v3));
|
|
size_t nsegs = std::max((uint32_t)(clen * tscale / 16), 16U);
|
|
// append cubic
|
|
Bezier bezier{v0, v1, v2, v3};
|
|
for (size_t i = 1; i <= nsegs; i++)
|
|
append(bezier.at((float)i / nsegs));
|
|
}
|
|
|
|
// trim source buffer
|
|
void trim(const WgVertexBuffer& buff, float beg, float end)
|
|
{
|
|
// empty buffer guard
|
|
if (buff.vcount == 0) return;
|
|
// initialize
|
|
float len_beg = buff.total() * beg;
|
|
float len_end = buff.total() * end;
|
|
// find points
|
|
size_t index_beg = buff.getIndexByLength(len_beg);
|
|
size_t index_end = buff.getIndexByLength(len_end);
|
|
float len_total_beg = buff.vleng[index_beg];
|
|
float len_total_end = buff.vleng[index_end];
|
|
float len_seg_beg = buff.vdist[index_beg];
|
|
float len_seg_end = buff.vdist[index_end];
|
|
// append points
|
|
float t_beg = len_seg_beg > 0.0f ? 1.0f - (len_total_beg - len_beg) / len_seg_beg : 0.0f;
|
|
float t_end = len_seg_end > 0.0f ? 1.0f - (len_total_end - len_end) / len_seg_end : 0.0f;
|
|
//t_beg == 1 handled in appendRange
|
|
if (index_beg > 0 && t_beg != 1.0f) append(lerp(buff.vbuff[index_beg-1], buff.vbuff[index_beg], t_beg));
|
|
appendRange(buff, index_beg, index_end);
|
|
//t_end == 0 handled in appendRange
|
|
if (index_end > 0 && t_end != 0.0f) append(lerp(buff.vbuff[index_end-1], buff.vbuff[index_end], t_end));
|
|
}
|
|
|
|
|
|
// decode path with callback for external prcesses
|
|
void decodePath(const RenderShape& rshape, bool update_dist, onPolylineFn onPolyline)
|
|
{
|
|
// decode path
|
|
reset(tscale);
|
|
size_t pntIndex = 0;
|
|
for (uint32_t cmdIndex = 0; cmdIndex < rshape.path.cmds.count; cmdIndex++) {
|
|
PathCommand cmd = rshape.path.cmds[cmdIndex];
|
|
if (cmd == PathCommand::MoveTo) {
|
|
// after path decoding we need to update distances and total length
|
|
if (update_dist) updateDistances();
|
|
if ((onPolyline) && (vcount > 0)) onPolyline(*this);
|
|
reset(tscale);
|
|
append(rshape.path.pts[pntIndex]);
|
|
pntIndex++;
|
|
} else if (cmd == PathCommand::LineTo) {
|
|
append(rshape.path.pts[pntIndex]);
|
|
pntIndex++;
|
|
} else if (cmd == PathCommand::Close) {
|
|
close();
|
|
// proceed path if close command is not the last command and next command is LineTo or CubicTo
|
|
if ((cmdIndex + 1 < rshape.path.cmds.count) &&
|
|
((rshape.path.cmds[cmdIndex + 1] == PathCommand::LineTo) ||
|
|
(rshape.path.cmds[cmdIndex + 1] == PathCommand::CubicTo))) {
|
|
// proceed current path
|
|
if (update_dist) updateDistances();
|
|
if ((vcount > 0) && (onPolyline)) onPolyline(*this);
|
|
// append closing point of current path as a first point of the new path
|
|
Point last_pt = last();
|
|
reset(tscale);
|
|
append(last_pt);
|
|
}
|
|
} else if (cmd == PathCommand::CubicTo) {
|
|
// append tesselated cubic spline with tscale param
|
|
appendCubic(vbuff[vcount - 1], rshape.path.pts[pntIndex + 0], rshape.path.pts[pntIndex + 1], rshape.path.pts[pntIndex + 2]);
|
|
pntIndex += 3;
|
|
}
|
|
}
|
|
// after path decoding we need to update distances and total length
|
|
if (update_dist) updateDistances();
|
|
if ((vcount > 0) && (onPolyline)) onPolyline(*this);
|
|
reset(tscale);
|
|
}
|
|
};
|
|
|
|
// simple indexed vertex buffer
|
|
struct WgVertexBufferInd
|
|
{
|
|
Point vbuff[WG_POINTS_COUNT*16];
|
|
Point tbuff[WG_POINTS_COUNT*16];
|
|
uint32_t ibuff[WG_POINTS_COUNT*16];
|
|
size_t vcount = 0;
|
|
size_t icount = 0;
|
|
float tscale = 1.0f;
|
|
|
|
// reset buffer
|
|
void reset(float scale)
|
|
{
|
|
icount = vcount = 0;
|
|
tscale = scale;
|
|
}
|
|
|
|
// get min and max values of the buffer
|
|
void getMinMax(Point& pmin, Point& pmax) const
|
|
{
|
|
if (vcount == 0) return;
|
|
pmax = pmin = vbuff[0];
|
|
for (size_t i = 1; i < vcount; i++) {
|
|
pmin = min(pmin, vbuff[i]);
|
|
pmax = max(pmax, vbuff[i]);
|
|
}
|
|
}
|
|
|
|
// append quad - two triangles formed from four points
|
|
void appendQuad(const Point& p0, const Point& p1, const Point& p2, const Point& p3)
|
|
{
|
|
// append vertexes
|
|
vbuff[vcount+0] = p0;
|
|
vbuff[vcount+1] = p1;
|
|
vbuff[vcount+2] = p2;
|
|
vbuff[vcount+3] = p3;
|
|
// append indexes
|
|
ibuff[icount+0] = vcount + 0;
|
|
ibuff[icount+1] = vcount + 1;
|
|
ibuff[icount+2] = vcount + 2;
|
|
ibuff[icount+3] = vcount + 1;
|
|
ibuff[icount+4] = vcount + 3;
|
|
ibuff[icount+5] = vcount + 2;
|
|
// update buffer
|
|
vcount += 4;
|
|
icount += 6;
|
|
}
|
|
|
|
// dash buffer by pattern
|
|
void appendStrokesDashed(const WgVertexBuffer& buff, const RenderStroke* rstroke)
|
|
{
|
|
// dashed buffer
|
|
WgVertexBuffer dashed;
|
|
dashed.reset(tscale);
|
|
// ignore single points polyline
|
|
if (buff.vcount < 2) return;
|
|
const float* dashPattern = rstroke->dashPattern;
|
|
size_t dashCnt = rstroke->dashCnt;
|
|
// starting state
|
|
uint32_t index_dash = 0;
|
|
float len_total = dashPattern[index_dash];
|
|
// iterate by polyline points
|
|
for (uint32_t i = 0; i < buff.vcount - 1; i++) {
|
|
// append current polyline point
|
|
if (index_dash % 2 == 0)
|
|
dashed.append(buff.vbuff[i]);
|
|
// move inside polyline segment
|
|
while(len_total < buff.vdist[i+1]) {
|
|
// get current point
|
|
float t = len_total / buff.vdist[i+1];
|
|
dashed.append(buff.vbuff[i] + (buff.vbuff[i+1] - buff.vbuff[i]) * t);
|
|
// update current state
|
|
index_dash = (index_dash + 1) % dashCnt;
|
|
len_total += dashPattern[index_dash];
|
|
// preceed stroke if dash
|
|
if (index_dash % 2 != 0) {
|
|
dashed.updateDistances();
|
|
appendStrokes(dashed, rstroke);
|
|
dashed.reset(tscale);
|
|
}
|
|
}
|
|
// update current subline length
|
|
len_total -= buff.vdist[i+1];
|
|
}
|
|
// draw last subline
|
|
if (index_dash % 2 == 0) {
|
|
dashed.append(buff.last());
|
|
dashed.updateDistances();
|
|
appendStrokes(dashed, rstroke);
|
|
}
|
|
}
|
|
|
|
// append buffer with optional offset
|
|
void appendBuffer(const WgVertexBuffer& buff, Point offset = Point{0.0f, 0.0f})
|
|
{
|
|
for (uint32_t i = 0; i < buff.vcount; i++ ) {
|
|
vbuff[vcount + i] = buff.vbuff[i] + offset;
|
|
ibuff[icount + i] = vcount + i;
|
|
}
|
|
vcount += buff.vcount;
|
|
icount += buff.vcount;
|
|
};
|
|
|
|
void appendLine(const Point& v0, const Point& v1, float dist, float halfWidth)
|
|
{
|
|
if(tvg::zero(dist)) return;
|
|
Point sub = v1 - v0;
|
|
Point nrm = { +sub.y / dist * halfWidth, -sub.x / dist * halfWidth };
|
|
appendQuad(v0 - nrm, v0 + nrm, v1 - nrm, v1 + nrm);
|
|
}
|
|
|
|
void appendBevel(const Point& v0, const Point& v1, const Point& v2, float dist1, float dist2, float halfWidth)
|
|
{
|
|
if(tvg::zero(dist1) || tvg::zero(dist2)) return;
|
|
Point sub1 = v1 - v0;
|
|
Point sub2 = v2 - v1;
|
|
Point nrm1 { +sub1.y / dist1 * halfWidth, -sub1.x / dist1 * halfWidth };
|
|
Point nrm2 { +sub2.y / dist2 * halfWidth, -sub2.x / dist2 * halfWidth };
|
|
appendQuad(v1 - nrm1, v1 + nrm1, v1 - nrm2, v1 + nrm2);
|
|
}
|
|
|
|
void appendMiter(const Point& v0, const Point& v1, const Point& v2, float dist1, float dist2, float halfWidth, float miterLimit)
|
|
{
|
|
if(tvg::zero(dist1) || tvg::zero(dist2)) return;
|
|
auto sub1 = v1 - v0;
|
|
auto sub2 = v2 - v1;
|
|
auto nrm1 = Point{+sub1.y / dist1, -sub1.x / dist1};
|
|
auto nrm2 = Point{+sub2.y / dist2, -sub2.x / dist2};
|
|
auto offset1 = nrm1 * halfWidth;
|
|
auto offset2 = nrm2 * halfWidth;
|
|
auto nrm = nrm1 + nrm2;
|
|
normalize(nrm);
|
|
float cosine = dot(nrm, nrm1);
|
|
if (tvg::zero(cosine)) return;
|
|
float angle = std::acos(dot(nrm1, -nrm2));
|
|
if (tvg::zero(angle) || tvg::equal(angle, MATH_PI)) return;
|
|
float miterRatio = 1.0f / (std::sin(angle) * 0.5f);
|
|
if (miterRatio <= miterLimit) {
|
|
appendQuad(v1 + nrm * (halfWidth / cosine), v1 + offset2, v1 + offset1, v1);
|
|
appendQuad(v1 - nrm * (halfWidth / cosine), v1 - offset2, v1 - offset1, v1);
|
|
} else {
|
|
appendQuad(v1 - offset1, v1 + offset2, v1 - offset2, v1 + offset1);
|
|
}
|
|
}
|
|
|
|
void appendSquare(Point v0, Point v1, float dist, float halfWidth)
|
|
{
|
|
if(tvg::zero(dist)) return;
|
|
Point sub = v1 - v0;
|
|
Point offset = sub / dist * halfWidth;
|
|
Point nrm = { +offset.y, -offset.x };
|
|
appendQuad(v1 - nrm, v1 + nrm, v1 + offset - nrm, v1 + offset + nrm);
|
|
}
|
|
|
|
void appendStrokes(const WgVertexBuffer& buff, const RenderStroke* rstroke)
|
|
{
|
|
assert(rstroke);
|
|
// empty buffer gueard
|
|
if (buff.vcount < 2) return;
|
|
float halfWidth = rstroke->width * 0.5f;
|
|
|
|
// append core lines
|
|
for (size_t i = 1; i < buff.vcount; i++)
|
|
appendLine(buff.vbuff[i-1], buff.vbuff[i], buff.vdist[i], halfWidth);
|
|
|
|
// append caps (square)
|
|
if ((rstroke->cap == StrokeCap::Square) && !buff.closed) {
|
|
appendSquare(buff.vbuff[1], buff.vbuff[0], buff.vdist[1], halfWidth);
|
|
appendSquare(buff.last(1), buff.last(0), buff.lastDist(0), halfWidth);
|
|
}
|
|
|
|
// append round joints and caps
|
|
if ((rstroke->join == StrokeJoin::Round) || (rstroke->cap == StrokeCap::Round)) {
|
|
// create mesh for circle
|
|
WgVertexBuffer circle;
|
|
circle.reset(buff.tscale);
|
|
circle.appendCircle(halfWidth);
|
|
// append caps (round)
|
|
if (rstroke->cap == StrokeCap::Round) {
|
|
appendBuffer(circle, buff.vbuff[0]);
|
|
// append ending cap if polyline is not closed
|
|
if (!buff.closed)
|
|
appendBuffer(circle, buff.last());
|
|
}
|
|
// append joints (round)
|
|
if (rstroke->join == StrokeJoin::Round) {
|
|
for (size_t i = 1; i < buff.vcount - 1; i++)
|
|
appendBuffer(circle, buff.vbuff[i]);
|
|
if (buff.closed) appendBuffer(circle, buff.last());
|
|
}
|
|
}
|
|
|
|
// append closed endings
|
|
if (buff.closed) {
|
|
// close by bevel
|
|
if (rstroke->join == StrokeJoin::Bevel)
|
|
appendBevel(buff.last(1), buff.vbuff[0], buff.vbuff[1], buff.lastDist(0), buff.vdist[1], halfWidth);
|
|
// close by mitter
|
|
else if (rstroke->join == StrokeJoin::Miter) {
|
|
appendMiter(buff.last(1), buff.vbuff[0], buff.vbuff[1], buff.lastDist(0), buff.vdist[1], halfWidth, rstroke->miterlimit);
|
|
}
|
|
}
|
|
|
|
// append joints (bevel)
|
|
if (rstroke->join == StrokeJoin::Bevel) {
|
|
for (size_t i = 1; i < buff.vcount - 1; i++)
|
|
appendBevel(buff.vbuff[i-1], buff.vbuff[i], buff.vbuff[i+1], buff.vdist[i], buff.vdist[i+1], halfWidth);
|
|
// append joints (mitter)
|
|
} else if (rstroke->join == StrokeJoin::Miter) {
|
|
for (size_t i = 1; i < buff.vcount - 1; i++)
|
|
appendMiter(buff.vbuff[i-1], buff.vbuff[i], buff.vbuff[i+1], buff.vdist[i], buff.vdist[i+1], halfWidth, rstroke->miterlimit);
|
|
}
|
|
}
|
|
};
|
|
|
|
#endif // _TVG_WG_GEOMETRY_H_
|