blob: 5f4f88762ffb279e0f45ac9c8c039f8c965e7510 [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include "rive/core/type_conversions.hpp"
#include "rive/math/raw_path_utils.hpp"
#include "rive/math/contour_measure.hpp"
#include "rive/math/math_types.hpp"
#include <cmath>
using namespace rive;
enum SegmentType { // must fit in 2 bits
kLine,
kQuad,
kCubic,
// unused for now
};
/*
* Inspired by Skia's SkContourMeasure
*/
ContourMeasure::ContourMeasure(std::vector<Segment>&& segs,
std::vector<Vec2D>&& pts,
float length,
bool isClosed) :
m_segments(std::move(segs)), m_points(std::move(pts)), m_length(length), m_isClosed(isClosed) {}
// Return index of the segment that contains distance,
// or the last segment if distance == m_distance
size_t ContourMeasure::findSegment(float distance) const {
assert(m_segments.front().m_distance > 0);
assert(m_segments.back().m_distance == m_length);
assert(distance >= 0 && distance <= m_length);
const Segment seg = {distance, 0, 0, 0};
auto iter = std::lower_bound(m_segments.begin(), m_segments.end(), seg);
assert(iter != m_segments.end());
assert(iter->m_distance >= distance);
assert(iter->m_ptIndex < m_points.size());
return iter - m_segments.begin();
}
static ContourMeasure::PosTan eval_quad(const Vec2D pts[], float t) {
assert(t >= 0 && t <= 1);
const EvalQuad eval(pts);
// Compute derivative as at + b
auto a = two(eval.a);
auto b = eval.b;
return {
eval(t),
(a * t + b).normalized(),
};
}
static ContourMeasure::PosTan eval_cubic(const Vec2D pts[], float t) {
assert(t >= 0 && t <= 1);
const EvalCubic eval(pts);
// Compute derivative as at^2 + bt + c;
auto a = eval.a * 3;
auto b = two(eval.b);
auto c = eval.c;
return {
eval(t),
((a * t + b) * t + c).normalized(),
};
}
void ContourMeasure::Segment::extract(RawPath* dst, const Vec2D pts[]) const {
pts += m_ptIndex;
switch (m_type) {
case SegmentType::kLine: dst->line(pts[1]); break;
case SegmentType::kQuad: dst->quad(pts[1], pts[2]); break;
case SegmentType::kCubic: dst->cubic(pts[1], pts[2], pts[3]); break;
}
}
void ContourMeasure::Segment::extract(RawPath* dst,
float fromT,
float toT,
const Vec2D pts[],
bool moveTo) const {
assert(fromT <= toT);
pts += m_ptIndex;
Vec2D tmp[4];
switch (m_type) {
case SegmentType::kLine:
line_extract(pts, fromT, toT, tmp);
if (moveTo) {
dst->move(tmp[0]);
}
dst->line(tmp[1]);
break;
case SegmentType::kQuad:
quad_extract(pts, fromT, toT, tmp);
if (moveTo) {
dst->move(tmp[0]);
}
dst->quad(tmp[1], tmp[2]);
break;
case SegmentType::kCubic:
cubic_extract(pts, fromT, toT, tmp);
if (moveTo) {
dst->move(tmp[0]);
}
dst->cubic(tmp[1], tmp[2], tmp[3]);
break;
}
}
ContourMeasure::PosTan ContourMeasure::getPosTan(float distance) const {
// specal-case end of the contour
if (distance >= m_length) {
size_t N = m_points.size();
assert(N > 1);
return {m_points[N - 1], (m_points[N - 1] - m_points[N - 2]).normalized()};
}
if (distance < 0) {
distance = 0;
}
size_t i = this->findSegment(distance);
assert(i < m_segments.size());
const auto seg = m_segments[i];
const float currD = seg.m_distance;
const float prevD = i > 0 ? m_segments[i - 1].m_distance : 0;
assert(prevD < currD);
assert(distance <= currD);
assert(distance >= prevD);
const float relD = (distance - prevD) / (currD - prevD);
assert(relD >= 0 && relD <= 1);
if (seg.m_type == SegmentType::kLine) {
assert(seg.m_ptIndex + 1 < m_points.size());
auto p0 = m_points[seg.m_ptIndex + 0];
auto p1 = m_points[seg.m_ptIndex + 1];
return {
Vec2D::lerp(p0, p1, relD),
(p1 - p0).normalized(),
};
}
float prevT = 0;
if (i > 0) {
auto prev = m_segments[i - 1];
if (prev.m_ptIndex == seg.m_ptIndex) {
prevT = prev.getT();
}
}
const float t = lerp(prevT, seg.getT(), relD);
assert(t >= 0 && t <= 1);
if (seg.m_type == SegmentType::kQuad) {
assert(seg.m_ptIndex + 2 < m_points.size());
return eval_quad(&m_points[seg.m_ptIndex], t);
} else {
assert(seg.m_type == SegmentType::kCubic);
assert(seg.m_ptIndex + 3 < m_points.size());
return eval_cubic(&m_points[seg.m_ptIndex], t);
}
}
static const ContourMeasure::Segment* next_segment_beginning(const ContourMeasure::Segment* seg) {
auto startingPtIndex = seg->m_ptIndex;
do {
seg += 1;
} while (seg->m_ptIndex == startingPtIndex);
return seg;
}
// Compute the (interpolated) t for a distance within the index'th segment
static float compute_t(Span<const ContourMeasure::Segment> segs, size_t index, float distance) {
const auto seg = segs[index];
assert(distance <= seg.m_distance);
float prevDist = 0, prevT = 0;
if (index > 0) {
const auto prev = segs[index - 1];
prevDist = prev.m_distance;
if (prev.m_ptIndex == seg.m_ptIndex) {
prevT = prev.getT();
}
}
assert(prevDist < seg.m_distance);
const auto ratio = (distance - prevDist) / (seg.m_distance - prevDist);
return lerp(prevT, seg.getT(), ratio);
}
void ContourMeasure::getSegment(float startDist,
float endDist,
RawPath* dst,
bool startWithMove) const {
// sanitize the inputs
startDist = std::max(0.f, startDist);
endDist = std::min(m_length, endDist);
if (startDist >= endDist) {
return;
}
const auto startIndex = this->findSegment(startDist);
const auto endIndex = this->findSegment(endDist);
const auto start = m_segments[startIndex];
const auto end = m_segments[endIndex];
const auto startT = compute_t(m_segments, startIndex, startDist);
const auto endT = compute_t(m_segments, endIndex, endDist);
if (start.m_ptIndex == end.m_ptIndex) {
start.extract(dst, startT, endT, m_points.data(), startWithMove);
} else {
start.extract(dst, startT, 1, m_points.data(), startWithMove);
// now scoop up all the segments after start, and before end
const auto* seg = next_segment_beginning(&m_segments[startIndex]);
while (seg->m_ptIndex != end.m_ptIndex) {
seg->extract(dst, m_points.data());
seg = next_segment_beginning(seg);
}
assert(seg->m_ptIndex == end.m_ptIndex);
end.extract(dst, 0, endT, m_points.data(), false);
}
}
void ContourMeasure::dump() const {
printf("length %g pts %zu segs %zu\n", m_length, m_points.size(), m_segments.size());
for (const auto& s : m_segments) {
printf(" %g %d %g %d\n", s.m_distance, s.m_ptIndex, s.getT(), s.m_type);
}
}
//////////////////////////////////////////////////////////////////////////////
constexpr auto kMaxDot30 = ContourMeasure::kMaxDot30;
static inline unsigned toDot30(float x) {
assert(x >= 0 && x < 1);
return (unsigned)(x * (1 << 30));
}
static void addSeg(std::vector<ContourMeasure::Segment>& array,
const ContourMeasure::Segment& seg,
bool required = false) {
if (array.size() > 0) {
const auto& last = array.back();
assert(last.m_distance <= seg.m_distance);
if (last.m_distance == seg.m_distance) {
assert(!required);
return;
}
}
array.push_back(seg);
}
// These add[SegmentType]Segs routines append intermediate segments for the curve.
// They assume the caller has set the initial segment (with t == 0), so they only
// add intermediates.
float ContourMeasureIter::addQuadSegs(std::vector<ContourMeasure::Segment>& segs,
const Vec2D pts[],
uint32_t ptIndex,
float distance) const {
const int count = computeApproximatingQuadLineSegments(pts, m_invTolerance);
const float dt = 1.0f / count;
const EvalQuad eval(pts);
float t = dt;
Vec2D prev = pts[0];
for (int i = 1; i < count; ++i) {
auto next = eval(t);
distance += (next - prev).length();
addSeg(segs, {distance, ptIndex, toDot30(t), SegmentType::kQuad});
prev = next;
t += dt;
}
distance += (pts[2] - prev).length();
addSeg(segs, {distance, ptIndex, kMaxDot30, SegmentType::kQuad});
return distance;
}
float ContourMeasureIter::addCubicSegs(std::vector<ContourMeasure::Segment>& segs,
const Vec2D pts[],
uint32_t ptIndex,
float distance) const {
const int count = computeApproximatingCubicLineSegments(pts, m_invTolerance);
const float dt = 1.0f / count;
const EvalCubic eval(pts);
float t = dt;
Vec2D prev = pts[0];
for (int i = 1; i < count; ++i) {
auto next = eval(t);
distance += (next - prev).length();
addSeg(segs, {distance, ptIndex, toDot30(t), SegmentType::kCubic});
prev = next;
t += dt;
}
distance += (pts[3] - prev).length();
addSeg(segs, {distance, ptIndex, kMaxDot30, SegmentType::kCubic});
return distance;
}
void ContourMeasureIter::reset(const RawPath& path, float tolerance) {
m_iter = path.begin();
m_end = path.end();
m_srcPoints = path.points().data();
constexpr float kMinTolerance = 1.0f / 16;
m_invTolerance = 1.0f / std::max(tolerance, kMinTolerance);
}
// Can return null if either it encountered an empty contour (length == 0)
// or the iterator is exhausted.
//
rcp<ContourMeasure> ContourMeasureIter::tryNext() {
std::vector<ContourMeasure::Segment> segs;
std::vector<Vec2D> pts;
float distance = 0;
bool isClosed = false;
for (; m_iter != m_end; ++m_iter) {
auto [verb, iterPts] = *m_iter;
if (verb == PathVerb::move) {
if (!pts.empty()) {
break; // We've alredy seen a move. Save this one for next time.
}
pts.push_back(iterPts[0]);
continue;
}
assert(!pts.empty()); // PathVerb::move should have occurred first, and added a point.
assert(!isClosed); // PathVerb::close is always followed by a move or nothing.
float prevDistance = distance;
const uint32_t ptIndex = castTo<uint32_t>(pts.size() - 1);
switch (verb) {
case PathVerb::line:
distance += (iterPts[1] - iterPts[0]).length();
if (distance > prevDistance) {
addSeg(segs, {distance, ptIndex, kMaxDot30, SegmentType::kLine}, true);
pts.push_back(iterPts[1]);
}
break;
case PathVerb::quad:
distance = this->addQuadSegs(segs, iterPts, ptIndex, distance);
if (distance > prevDistance) {
pts.push_back(iterPts[1]);
pts.push_back(iterPts[2]);
}
break;
case PathVerb::cubic:
distance = this->addCubicSegs(segs, iterPts, ptIndex, distance);
if (distance > prevDistance) {
pts.push_back(iterPts[1]);
pts.push_back(iterPts[2]);
pts.push_back(iterPts[3]);
}
break;
case PathVerb::close: {
auto first = pts.front();
distance += (first - iterPts[0]).length();
if (distance > prevDistance) {
addSeg(segs, {distance, ptIndex, kMaxDot30, SegmentType::kLine}, true);
pts.push_back(first);
}
isClosed = true;
} break;
case PathVerb::move: RIVE_UNREACHABLE; // Handled above.
}
}
if (distance == 0 || pts.size() < 2) {
return nullptr;
}
return rcp<ContourMeasure>(
new ContourMeasure(std::move(segs), std::move(pts), distance, isClosed));
}
rcp<ContourMeasure> ContourMeasureIter::next() {
rcp<ContourMeasure> cm;
for (;;) {
if ((cm = this->tryNext())) {
break;
}
if (m_iter == m_end) {
break;
}
}
return cm;
}