blob: 6399d6c596c706bb4a5cbf9200bd0e68aaf9cb78 [file] [log] [blame]
#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) {
hull[0] = Vec2D::lerp(from, fromOut, t);
hull[1] = Vec2D::lerp(fromOut, toIn, t);
hull[2] = Vec2D::lerp(toIn, to, t);
hull[3] = Vec2D::lerp(hull[0], hull[1], t);
hull[4] = Vec2D::lerp(hull[1], hull[2], t);
hull[5] = Vec2D::lerp(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) {
const Vec2D oneThird = Vec2D::lerp(from, to, 1.0f / 3.0f),
twoThird = Vec2D::lerp(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++) {
m_TransformedPoints[i] = transform * m_Points[i];
}
// 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 = to - from;
if (moveTo) {
Vec2D point = from + dir * startT;
result->moveTo(point[0], point[1]);
}
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); }