blob: 0aa804af161f5d65fecde904ecc7731bf37cad3c [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 "rive/math/wangs_formula.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));
}
// 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(ContourMeasure::Segment* segs,
const Vec2D pts[],
uint32_t segmentCount,
uint32_t ptIndex,
float distance) const
{
const float dt = 1.f / segmentCount;
const EvalQuad eval(pts);
float t = dt;
Vec2D prev = pts[0];
for (uint32_t i = 1; i < segmentCount; ++i)
{
auto next = eval(t);
distance += (next - prev).length();
*segs++ = {distance, ptIndex, toDot30(t), SegmentType::kQuad};
prev = next;
t += dt;
}
distance += (pts[2] - prev).length();
*segs++ = {distance, ptIndex, kMaxDot30, SegmentType::kQuad};
return distance;
}
float ContourMeasureIter::addCubicSegs(ContourMeasure::Segment* segs,
const Vec2D pts[],
uint32_t segmentCount,
uint32_t ptIndex,
float distance) const
{
const float dt = 1.f / segmentCount;
const EvalCubic eval(pts);
float t = dt;
Vec2D prev = pts[0];
for (uint32_t i = 1; i < segmentCount; ++i)
{
auto next = eval(t);
distance += (next - prev).length();
*segs++ = {distance, ptIndex, toDot30(t), SegmentType::kCubic};
prev = next;
t += dt;
}
distance += (pts[3] - prev).length();
*segs++ = {distance, ptIndex, kMaxDot30, SegmentType::kCubic};
return distance;
}
void ContourMeasureIter::rewind(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);
m_segmentCounts.resize(path.verbs().count());
}
// Can return null if either it encountered an empty contour (length == 0)
// or the iterator is exhausted.
//
rcp<ContourMeasure> ContourMeasureIter::tryNext()
{
assert(m_iter == m_end || m_iter.verb() == PathVerb::move);
// Advance to the first line or curve.
Vec2D p0;
for (;; ++m_iter)
{
if (m_iter == m_end)
{
return nullptr;
}
switch (m_iter.verb())
{
case PathVerb::move:
p0 = m_iter.movePt();
RIVE_FALLTHROUGH;
case PathVerb::close:
continue;
default:
break;
}
break;
}
// Pass 1: count segments.
uint32_t* nextSegCount = m_segmentCounts.data();
size_t segmentCountInCurves = 0;
size_t lineCount = 0;
bool isClosed = false;
RawPath::Iter endOfContour = m_end;
for (auto it = m_iter; it != m_end; ++it)
{
// Arbirtary limit to keep our segmenting tractable.
constexpr static uint32_t kMaxSegments = 100;
switch (it.verb())
{
case PathVerb::move:
endOfContour = it; // This move belongs to the next contour.
break;
case PathVerb::line:
++lineCount;
continue;
case PathVerb::quad:
{
assert(nextSegCount < m_segmentCounts.data() + m_segmentCounts.size());
uint32_t segmentCount = static_cast<uint32_t>(
ceilf(wangs_formula::quadratic(it.quadPts(), m_invTolerance)));
segmentCount = std::max(1u, std::min(segmentCount, kMaxSegments));
segmentCountInCurves += segmentCount;
*nextSegCount++ = segmentCount;
continue;
}
case PathVerb::cubic:
{
assert(nextSegCount < m_segmentCounts.data() + m_segmentCounts.size());
uint32_t segmentCount = static_cast<uint32_t>(
ceilf(ceilf(wangs_formula::cubic(it.cubicPts(), m_invTolerance))));
segmentCount = std::max(1u, std::min(segmentCount, kMaxSegments));
segmentCountInCurves += segmentCount;
*nextSegCount++ = segmentCount;
continue;
}
case PathVerb::close:
if (it.ptBeforeClose() != p0)
{
++lineCount;
}
isClosed = true;
continue;
}
break;
}
// Pass 2: emit segments.
std::vector<ContourMeasure::Segment> segs;
segs.resize(segmentCountInCurves + lineCount);
ContourMeasure::Segment* nextSeg = segs.data();
nextSegCount = m_segmentCounts.data();
float distance = 0;
uint32_t ptIndex = 0;
bool duplicateP0 = false;
for (auto it = m_iter; it != endOfContour; ++it)
{
switch (it.verb())
{
case PathVerb::move:
RIVE_UNREACHABLE();
case PathVerb::line:
distance += (it.linePts()[1] - it.linePts()[0]).length();
*nextSeg++ = {distance, ptIndex, kMaxDot30, SegmentType::kLine};
++ptIndex;
break;
case PathVerb::quad:
{
const uint32_t n = *nextSegCount++;
distance = addQuadSegs(nextSeg, it.quadPts(), n, ptIndex, distance);
nextSeg += n;
ptIndex += 2;
break;
}
case PathVerb::cubic:
{
const uint32_t n = *nextSegCount++;
distance = addCubicSegs(nextSeg, it.cubicPts(), n, ptIndex, distance);
nextSeg += n;
ptIndex += 3;
break;
}
case PathVerb::close:
if (it.ptBeforeClose() != p0)
{
distance += (p0 - it.ptBeforeClose()).length();
*nextSeg++ = {distance, ptIndex, kMaxDot30, SegmentType::kLine};
++ptIndex;
duplicateP0 = true;
}
assert(isClosed);
}
}
assert(nextSeg == segs.data() + segs.size());
// Copy out points.
std::vector<Vec2D> pts;
pts.reserve(1 + ptIndex);
pts.insert(pts.end(), m_iter.rawPtsPtr() - 1, endOfContour.rawPtsPtr());
if (duplicateP0)
{
pts.push_back(p0);
}
assert(pts.size() == 1 + ptIndex);
m_iter = endOfContour;
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;
}