| #include "rive/shapes/metrics_path.hpp" |
| #include "rive/renderer.hpp" |
| #include <math.h> |
| |
| using namespace rive; |
| |
| float clamp(float v, float lo, float hi) |
| { |
| if (v < lo) |
| { |
| return lo; |
| } |
| else if (v > hi) |
| { |
| return hi; |
| } |
| return v; |
| } |
| |
| void MetricsPath::reset() |
| { |
| m_ComputedLength = 0.0f; |
| m_CubicSegments.clear(); |
| m_Points.clear(); |
| m_Parts.clear(); |
| m_Lengths.clear(); |
| m_Paths.clear(); |
| } |
| |
| void MetricsPath::addPath(CommandPath* path, const Mat2D& transform) |
| { |
| MetricsPath* metricsPath = reinterpret_cast<MetricsPath*>(path); |
| m_ComputedLength += metricsPath->computeLength(transform); |
| m_Paths.emplace_back(metricsPath); |
| } |
| |
| void MetricsPath::moveTo(float x, float y) |
| { |
| assert(m_Points.size() == 0); |
| m_Points.emplace_back(Vec2D(x, y)); |
| } |
| |
| void MetricsPath::lineTo(float x, float y) |
| { |
| m_Parts.push_back(PathPart(0, m_Points.size())); |
| m_Points.emplace_back(Vec2D(x, y)); |
| } |
| |
| void MetricsPath::cubicTo( |
| float ox, float oy, float ix, float iy, float x, float y) |
| { |
| m_Parts.push_back(PathPart(1, m_Points.size())); |
| m_Points.emplace_back(Vec2D(ox, oy)); |
| m_Points.emplace_back(Vec2D(ix, iy)); |
| m_Points.emplace_back(Vec2D(x, y)); |
| } |
| |
| void MetricsPath::close() {} |
| |
| static void computeHull(const Vec2D& from, |
| const Vec2D& fromOut, |
| const Vec2D& toIn, |
| const Vec2D& to, |
| float t, |
| Vec2D* hull) |
| { |
| Vec2D::lerp(hull[0], from, fromOut, t); |
| Vec2D::lerp(hull[1], fromOut, toIn, t); |
| Vec2D::lerp(hull[2], toIn, to, t); |
| |
| Vec2D::lerp(hull[3], hull[0], hull[1], t); |
| Vec2D::lerp(hull[4], hull[1], hull[2], t); |
| |
| Vec2D::lerp(hull[5], hull[3], hull[4], t); |
| } |
| |
| static const float minSegmentLength = 0.05f; |
| static const float distTooFar = 1.0f; |
| |
| static bool tooFar(const Vec2D& a, const Vec2D& b) |
| { |
| return std::max(std::abs(a[0] - b[0]), std::abs(a[1] - b[1])) > distTooFar; |
| } |
| |
| static bool shouldSplitCubic(const Vec2D& from, |
| const Vec2D& fromOut, |
| const Vec2D& toIn, |
| const Vec2D& to) |
| { |
| Vec2D oneThird, twoThird; |
| Vec2D::lerp(oneThird, from, to, 1.0f / 3.0f); |
| Vec2D::lerp(twoThird, from, to, 2.0f / 3.0f); |
| return tooFar(fromOut, oneThird) || tooFar(toIn, twoThird); |
| } |
| |
| static float segmentCubic(const Vec2D& from, |
| const Vec2D& fromOut, |
| const Vec2D& toIn, |
| const Vec2D& to, |
| float runningLength, |
| float t1, |
| float t2, |
| std::vector<CubicSegment>& segments) |
| { |
| |
| if (shouldSplitCubic(from, fromOut, toIn, to)) |
| { |
| float halfT = (t1 + t2) / 2.0f; |
| |
| Vec2D hull[6]; |
| computeHull(from, fromOut, toIn, to, 0.5f, hull); |
| |
| runningLength = segmentCubic(from, |
| hull[0], |
| hull[3], |
| hull[5], |
| runningLength, |
| t1, |
| halfT, |
| segments); |
| runningLength = segmentCubic( |
| hull[5], hull[4], hull[2], to, runningLength, halfT, t2, segments); |
| } |
| else |
| { |
| float length = Vec2D::distance(from, to); |
| runningLength += length; |
| if (length > minSegmentLength) |
| { |
| segments.emplace_back(CubicSegment(t2, runningLength)); |
| } |
| } |
| return runningLength; |
| } |
| |
| float MetricsPath::computeLength(const Mat2D& transform) |
| { |
| // If the pre-computed length is still valid (transformed with the same |
| // transform) just return that. |
| if (!m_Lengths.empty() && transform == m_ComputedLengthTransform) |
| { |
| return m_ComputedLength; |
| } |
| m_ComputedLengthTransform = transform; |
| m_Lengths.clear(); |
| m_CubicSegments.clear(); |
| |
| // We have to dupe the transformed points as we're not sure whether just the |
| // transform is changing (path may not have been reset but got added with |
| // another transform). |
| m_TransformedPoints.resize(m_Points.size()); |
| for (size_t i = 0, l = m_Points.size(); i < l; i++) |
| { |
| Vec2D::transform(m_TransformedPoints[i], m_Points[i], transform); |
| } |
| |
| // Should never have subPaths with more subPaths (Skia allows this but for |
| // Rive this isn't necessary and it keeps things simpler). |
| assert(m_Paths.empty()); |
| const Vec2D* pen = &m_TransformedPoints[0]; |
| int idx = 1; |
| float length = 0.0f; |
| |
| for (PathPart& part : m_Parts) |
| { |
| switch (part.type) |
| { |
| case PathPart::line: |
| { |
| const Vec2D& point = m_TransformedPoints[idx++]; |
| |
| float partLength = Vec2D::distance(*pen, point); |
| m_Lengths.push_back(partLength); |
| pen = &point; |
| length += partLength; |
| break; |
| } |
| // Anything above 0 is the number of cubic parts... |
| default: |
| { |
| // Subdivide as necessary... |
| |
| // push in the parts |
| |
| const Vec2D& from = pen[0]; |
| const Vec2D& fromOut = pen[1]; |
| const Vec2D& toIn = pen[2]; |
| const Vec2D& to = pen[3]; |
| |
| idx += 3; |
| pen = &to; |
| |
| int index = (int)m_CubicSegments.size(); |
| part.type = index + 1; |
| float partLength = segmentCubic( |
| from, fromOut, toIn, to, 0.0f, 0.0f, 1.0f, m_CubicSegments); |
| m_Lengths.push_back(partLength); |
| length += partLength; |
| part.numSegments = m_CubicSegments.size() - index; |
| break; |
| } |
| } |
| } |
| m_ComputedLength = length; |
| return length; |
| } |
| |
| void MetricsPath::trim(float startLength, |
| float endLength, |
| bool moveTo, |
| RenderPath* result) |
| { |
| assert(endLength >= startLength); |
| if (!m_Paths.empty()) |
| { |
| m_Paths.front()->trim(startLength, endLength, moveTo, result); |
| return; |
| } |
| if (startLength == endLength) |
| { |
| // nothing to trim. |
| return; |
| } |
| // We need to find the first part to trim. |
| float length = 0.0f; |
| |
| int partCount = (int)m_Parts.size(); |
| int firstPartIndex = -1, lastPartIndex = partCount - 1; |
| float startT = 0.0f, endT = 1.0f; |
| // Find first part. |
| for (int i = 0; i < partCount; i++) |
| { |
| float partLength = m_Lengths[i]; |
| if (length + partLength > startLength) |
| { |
| firstPartIndex = i; |
| startT = (startLength - length) / partLength; |
| break; |
| } |
| length += partLength; |
| } |
| if (firstPartIndex == -1) |
| { |
| // Couldn't find it. |
| return; |
| } |
| |
| // Find last part. |
| for (int i = firstPartIndex; i < partCount; i++) |
| { |
| float partLength = m_Lengths[i]; |
| if (length + partLength >= endLength) |
| { |
| lastPartIndex = i; |
| endT = (endLength - length) / partLength; |
| break; |
| } |
| length += partLength; |
| } |
| |
| // Lets make sur we're between 0 & 1f on both start & end. |
| startT = clamp(startT, 0.0f, 1.0f); |
| endT = clamp(endT, 0.0f, 1.0f); |
| |
| if (firstPartIndex == lastPartIndex) |
| { |
| extractSubPart(firstPartIndex, startT, endT, moveTo, result); |
| } |
| else |
| { |
| extractSubPart(firstPartIndex, startT, 1.0f, moveTo, result); |
| for (int i = firstPartIndex + 1; i < lastPartIndex; i++) |
| { |
| // add entire part... |
| const PathPart& part = m_Parts[i]; |
| switch (part.type) |
| { |
| case PathPart::line: |
| { |
| const Vec2D& point = m_TransformedPoints[part.offset]; |
| result->lineTo(point[0], point[1]); |
| break; |
| } |
| default: |
| { |
| const Vec2D& point1 = m_TransformedPoints[part.offset]; |
| const Vec2D& point2 = m_TransformedPoints[part.offset + 1]; |
| const Vec2D& point3 = m_TransformedPoints[part.offset + 2]; |
| result->cubicTo(point1[0], |
| point1[1], |
| point2[0], |
| point2[1], |
| point3[0], |
| point3[1]); |
| break; |
| } |
| } |
| } |
| extractSubPart(lastPartIndex, 0.0f, endT, false, result); |
| } |
| } |
| |
| float lerp(float from, float to, float f) { return from + f * (to - from); } |
| |
| void MetricsPath::extractSubPart( |
| int index, float startT, float endT, bool moveTo, RenderPath* result) |
| { |
| assert(startT >= 0.0f && startT <= 1.0f && endT >= 0.0f && endT <= 1.0f); |
| const PathPart& part = m_Parts[index]; |
| switch (part.type) |
| { |
| case PathPart::line: |
| { |
| const Vec2D& from = m_TransformedPoints[part.offset - 1]; |
| const Vec2D& to = m_TransformedPoints[part.offset]; |
| Vec2D dir; |
| Vec2D::subtract(dir, to, from); |
| if (moveTo) |
| { |
| Vec2D point; |
| Vec2D::scaleAndAdd(point, from, dir, startT); |
| result->moveTo(point[0], point[1]); |
| } |
| Vec2D::scaleAndAdd(dir, from, dir, endT); |
| result->lineTo(dir[0], dir[1]); |
| |
| break; |
| } |
| default: |
| { |
| auto startingSegmentIndex = part.type - 1; |
| auto startEndSegmentIndex = startingSegmentIndex; |
| auto endingSegmentIndex = startingSegmentIndex + part.numSegments; |
| |
| // Find cubicStartT and cubicEndT |
| float length = m_Lengths[index]; |
| if (startT != 0.0f) |
| { |
| float startLength = startT * length; |
| for (int si = startingSegmentIndex; si < endingSegmentIndex; |
| si++) |
| { |
| const CubicSegment& segment = m_CubicSegments[si]; |
| if (segment.length >= startLength) |
| { |
| if (si == startingSegmentIndex) |
| { |
| startT = segment.t * (startLength / segment.length); |
| } |
| else |
| { |
| float previousLength = |
| m_CubicSegments[si - 1].length; |
| |
| float t = (startLength - previousLength) / |
| (segment.length - previousLength); |
| startT = |
| lerp(m_CubicSegments[si - 1].t, segment.t, t); |
| } |
| // Help out the ending segment finder by setting its |
| // start to where we landed while finding the first |
| // segment, that way it can skip a bunch of work. |
| startEndSegmentIndex = si; |
| break; |
| } |
| } |
| } |
| |
| if (endT != 1.0f) |
| { |
| float endLength = endT * length; |
| for (int si = startEndSegmentIndex; si < endingSegmentIndex; |
| si++) |
| { |
| const CubicSegment& segment = m_CubicSegments[si]; |
| if (segment.length >= endLength) |
| { |
| if (si == startingSegmentIndex) |
| { |
| endT = segment.t * (endLength / segment.length); |
| } |
| else |
| { |
| float previousLength = |
| m_CubicSegments[si - 1].length; |
| |
| float t = (endLength - previousLength) / |
| (segment.length - previousLength); |
| endT = |
| lerp(m_CubicSegments[si - 1].t, segment.t, t); |
| } |
| break; |
| } |
| } |
| } |
| |
| Vec2D hull[6]; |
| |
| const Vec2D& from = m_TransformedPoints[part.offset - 1]; |
| const Vec2D& fromOut = m_TransformedPoints[part.offset]; |
| const Vec2D& toIn = m_TransformedPoints[part.offset + 1]; |
| const Vec2D& to = m_TransformedPoints[part.offset + 2]; |
| |
| if (startT == 0.0f) |
| { |
| // Start is 0, so split at end and keep the left side. |
| computeHull(from, fromOut, toIn, to, endT, hull); |
| if (moveTo) |
| { |
| result->moveTo(from[0], from[1]); |
| } |
| result->cubicTo(hull[0][0], |
| hull[0][1], |
| hull[3][0], |
| hull[3][1], |
| hull[5][0], |
| hull[5][1]); |
| } |
| else |
| { |
| // Split at start since it's non 0. |
| computeHull(from, fromOut, toIn, to, startT, hull); |
| if (moveTo) |
| { |
| // Move to first point on the right side. |
| result->moveTo(hull[5][0], hull[5][1]); |
| } |
| if (endT == 1.0f) |
| { |
| // End is 1, so no further split is necessary just cubicTo |
| // the remaining right side. |
| result->cubicTo(hull[4][0], |
| hull[4][1], |
| hull[2][0], |
| hull[2][1], |
| to[0], |
| to[1]); |
| } |
| else |
| { |
| // End is not 1, so split again and cubic to the left side |
| // of the split and remap endT to the new curve range |
| computeHull(hull[5], |
| hull[4], |
| hull[2], |
| to, |
| (endT - startT) / (1.0f - startT), |
| hull); |
| |
| result->cubicTo(hull[0][0], |
| hull[0][1], |
| hull[3][0], |
| hull[3][1], |
| hull[5][0], |
| hull[5][1]); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| RenderMetricsPath::RenderMetricsPath() : m_RenderPath(makeRenderPath()) {} |
| RenderMetricsPath::~RenderMetricsPath() { delete m_RenderPath; } |
| |
| void RenderMetricsPath::addPath(CommandPath* path, const Mat2D& transform) |
| { |
| MetricsPath::addPath(path, transform); |
| m_RenderPath->addPath(path->renderPath(), transform); |
| } |
| |
| void RenderMetricsPath::reset() |
| { |
| MetricsPath::reset(); |
| m_RenderPath->reset(); |
| } |
| |
| void RenderMetricsPath::moveTo(float x, float y) |
| { |
| MetricsPath::moveTo(x, y); |
| m_RenderPath->moveTo(x, y); |
| } |
| |
| void RenderMetricsPath::lineTo(float x, float y) |
| { |
| MetricsPath::lineTo(x, y); |
| m_RenderPath->lineTo(x, y); |
| } |
| |
| void RenderMetricsPath::cubicTo( |
| float ox, float oy, float ix, float iy, float x, float y) |
| { |
| MetricsPath::cubicTo(ox, oy, ix, iy, x, y); |
| m_RenderPath->cubicTo(ox, oy, ix, iy, x, y); |
| } |
| |
| void RenderMetricsPath::close() |
| { |
| MetricsPath::close(); |
| m_RenderPath->close(); |
| } |
| |
| void RenderMetricsPath::fillRule(FillRule value) |
| { |
| m_RenderPath->fillRule(value); |
| } |