blob: f94d83ba74260550108a288eda329d73a03c7ee6 [file]
/*
* Copyright 2022 Rive
*/
#include "rive/math/hit_test.hpp"
#include "rive/math/mat2d.hpp"
#include <algorithm>
#include <assert.h>
#include <cmath>
// Should we make this an option at runtime?
#define CULL_BOUNDS true
using namespace rive;
static inline float graphics_roundf(float x) { return std::floor(x + 0.5f); }
static inline int graphics_round(float x) { return (int)graphics_roundf(x); }
struct Point
{
float x, y;
Point() {}
Point(float xx, float yy) : x(xx), y(yy) {}
Point(const Vec2D& src) : x(src.x), y(src.y) {}
Point operator+(Point v) const { return {x + v.x, y + v.y}; }
Point operator-(Point v) const { return {x - v.x, y - v.y}; }
Point& operator+=(Point v)
{
*this = *this + v;
return *this;
}
Point& operator-=(Point v)
{
*this = *this - v;
return *this;
}
friend Point operator*(Point v, float s) { return {v.x * s, v.y * s}; }
friend Point operator*(float s, Point v) { return {v.x * s, v.y * s}; }
};
template <typename T> T ave(T a, T b) { return lerp(a, b, 0.5f); }
static void append_line(
const float height,
Point p0,
Point p1,
float m,
int winding,
int delta[],
int iwidth)
{
assert(winding == 1 || winding == -1);
int top = graphics_round(p0.y);
int bottom = graphics_round(p1.y);
if (top == bottom)
{
return;
}
assert(top < bottom);
assert(top >= 0);
assert((float)bottom <= height);
// we add 0.5 at the end to pre-round the values
float x = p0.x + m * (top - p0.y + 0.5f) + 0.5f;
int* row = delta + top * iwidth;
for (int y = top; y < bottom; ++y)
{
int ix = (int)std::max(x, 0.0f);
if (ix < iwidth)
{
row[ix] += winding;
}
x += m;
row += iwidth;
}
}
static void clip_line(
const float height,
Point p0,
Point p1,
int delta[],
const int iwidth)
{
if (p0.y == p1.y)
{
return;
}
int winding = 1;
if (p0.y > p1.y)
{
winding = -1;
std::swap(p0, p1);
}
// now we're monotonic in Y: p0 <= p1
if (p1.y <= 0 || p0.y >= height)
{
return;
}
const float m = (float)(p1.x - p0.x) / (p1.y - p0.y);
if (p0.y < 0)
{
p0.x += m * (0 - p0.y);
p0.y = 0;
}
if (p1.y > height)
{
p1.x += m * (height - p1.y);
p1.y = height;
}
assert(p0.y <= p1.y);
assert(p0.y >= 0);
assert(p1.y <= height);
append_line(height, p0, p1, m, winding, delta, iwidth);
}
#define MAX_CURVE_SEGMENTS (1 << 8)
static int compute_cubic_segments(Point a, Point b, Point c, Point d)
{
Point abc = a - b - b + c;
Point bcd = b - c - c + d;
float dx = std::max(std::abs(abc.x), std::abs(bcd.x));
float dy = std::max(std::abs(abc.y), std::abs(bcd.y));
float dist = sqrtf(dx * dx + dy * dy);
// count = sqrt(6*dist / 8*tol)
// tol = 0.25
// count = sqrt(3*dist)
float count = sqrtf(3 * dist);
return std::max(1, std::min((int)ceilf(count), MAX_CURVE_SEGMENTS));
}
// cubic a(1-t)^3 + 3bt(1-t)^2 + 3c(1-t)t^2 + dt^3
// becomes
// At^3 + Bt^2 + Ct + D
//
struct CubicCoeff
{
Point A, B, C, D;
// a(1-t)^3 + 3bt(1-t)^2 + 3ct^2(1-t) + dt^3
// a - 3at + 3at^2 - at^3 a(1 - 3t + 3t^2 - t^3)
// 3bt - 6bt^2 + 3bt^3 3b( t - 2t^2 + t^3)
// 3ct^2 - 3ct^3 3c( t^2 - t^3)
// dt^3 d( t^3)
// ...
// D + Ct + Bt^2 + At^3
//
CubicCoeff(Point a, Point b, Point c, Point d)
{
A = (d - a) + 3.0f * (b - c);
B = 3.0f * ((c - b) + (a - b));
C = 3.0f * (b - a);
D = a;
}
Point eval(float t) const { return ((A * t + B) * t + C) * t + D; }
};
////////////////////////////////////////////
void HitTester::reset() { m_DW.clear(); }
void HitTester::reset(const IAABB& clip)
{
m_offset = Vec2D{(float)clip.left, (float)clip.top};
m_height = (float)clip.height();
m_IWidth = clip.width();
m_IHeight = clip.height();
m_DW.resize(m_IWidth * m_IHeight);
for (size_t i = 0; i < m_DW.size(); ++i)
{
m_DW[i] = 0;
}
m_ExpectsMove = true;
}
void HitTester::move(Vec2D v)
{
if (!m_ExpectsMove)
{
this->close();
}
m_First = m_Prev = v - m_offset;
m_ExpectsMove = false;
}
void HitTester::line(Vec2D v)
{
assert(!m_ExpectsMove);
v = v - m_offset;
clip_line(m_height, m_Prev, v, m_DW.data(), m_IWidth);
m_Prev = v;
}
void HitTester::quad(Vec2D b, Vec2D c)
{
assert(!m_ExpectsMove);
m_Prev = c;
}
static bool quickRejectCubic(float height, Point a, Point b, Point c, Point d)
{
const float h = height;
return (a.y <= 0 && b.y <= 0 && c.y <= 0 && d.y <= 0) ||
(a.y >= h && b.y >= h && c.y >= h && d.y >= h);
}
struct CubicChop
{
Vec2D storage[7];
CubicChop(Vec2D a, Vec2D b, Vec2D c, Vec2D d)
{
auto ab = ave(a, b);
auto bc = ave(b, c);
auto cd = ave(c, d);
auto abc = ave(ab, bc);
auto bcd = ave(bc, cd);
storage[0] = a;
storage[1] = ab;
storage[2] = abc;
storage[3] = ave(abc, bcd);
storage[4] = bcd;
storage[5] = cd;
storage[6] = d;
}
Vec2D operator[](unsigned index) const
{
assert(index < 7);
return storage[index];
}
};
// Trial and error to pick a good value for this.
//
// Subdivision and recursion have their own cost, so at some point
// just evaluating the cubic (count) times is cheaper than continuing
// to chop.
//
// The key win is quickRejectCubic. This is how we save time over just
// evaluating the cubic up front.
//
#define MAX_LOCAL_SEGMENTS 16
void HitTester::recurse_cubic(Vec2D b, Vec2D c, Vec2D d, int count)
{
if (quickRejectCubic(m_height, m_Prev, b, c, d))
{
m_Prev = d;
return;
}
if (count > MAX_LOCAL_SEGMENTS)
{
CubicChop chop(m_Prev, b, c, d);
const int newCount = (count + 1) >> 1;
assert(newCount < count);
this->recurse_cubic(chop[1], chop[2], chop[3], newCount);
this->recurse_cubic(chop[4], chop[5], chop[6], newCount);
}
else
{
const float dt = 1.0f / (float)count;
float t = dt;
CubicCoeff cube(m_Prev, b, c, d);
// we don't need the first point eval(0) or the last eval(1)
Point prev = m_Prev;
for (int i = 1; i < count - 1; ++i)
{
auto next = cube.eval(t);
clip_line(m_height, prev, next, m_DW.data(), m_IWidth);
prev = next;
t += dt;
}
clip_line(m_height, prev, d, m_DW.data(), m_IWidth);
m_Prev = d;
}
}
void HitTester::cubic(Vec2D b, Vec2D c, Vec2D d)
{
assert(!m_ExpectsMove);
b = b - m_offset;
c = c - m_offset;
d = d - m_offset;
if (quickRejectCubic(m_height, m_Prev, b, c, d))
{
m_Prev = d;
return;
}
const int count = compute_cubic_segments(m_Prev, b, c, d);
this->recurse_cubic(b, c, d, count);
}
void HitTester::close()
{
assert(!m_ExpectsMove);
clip_line(m_height, m_Prev, m_First, m_DW.data(), m_IWidth);
m_ExpectsMove = true;
}
void HitTester::addRect(const AABB& rect, const Mat2D& xform, PathDirection dir)
{
const Vec2D pts[] = {
xform * Vec2D{rect.left(), rect.top()},
xform * Vec2D{rect.right(), rect.top()},
xform * Vec2D{rect.right(), rect.bottom()},
xform * Vec2D{rect.left(), rect.bottom()},
};
move(pts[0]);
if (dir == PathDirection::clockwise)
{
line(pts[1]);
line(pts[2]);
line(pts[3]);
}
else
{
line(pts[3]);
line(pts[2]);
line(pts[1]);
}
close();
}
bool HitTester::test(FillRule rule)
{
if (!m_ExpectsMove)
{
this->close();
}
const int mask = (rule == rive::FillRule::nonZero) ? -1 : 1;
int nonzero = 0;
for (auto m : m_DW)
{
nonzero |= (m & mask);
}
return nonzero != 0;
}
/////////////////////////
bool HitTester::testMesh(Vec2D pt, Span<Vec2D> verts, Span<uint16_t> indices)
{
if (verts.size() < 3)
{
return false;
}
// Test against the bounds of the entire mesh
// Make this optional?
if (CULL_BOUNDS)
{
const auto bounds = AABB(verts);
if (bounds.bottom() < pt.y || pt.y < bounds.top() ||
bounds.right() < pt.x || pt.x < bounds.left())
{
return false;
}
}
for (size_t i = 0; i < indices.size(); i += 3)
{
const auto a = verts[indices[i + 0]];
const auto b = verts[indices[i + 1]];
const auto c = verts[indices[i + 2]];
// A collapsed triangle covers nothing, but every point passes the sign
// test below against one: its cross products are all zero.
if (Vec2D::cross(b - a, c - a) == 0.0f)
{
continue;
}
auto pa = a - pt;
auto pb = b - pt;
auto pc = c - pt;
const float ab = Vec2D::cross(pa, pb);
const float bc = Vec2D::cross(pb, pc);
const float ca = Vec2D::cross(pc, pa);
// Inside or on an edge, whichever way the triangle winds, so triangles
// sharing an edge leave no seam between them.
if ((ab >= 0.0f && bc >= 0.0f && ca >= 0.0f) ||
(ab <= 0.0f && bc <= 0.0f && ca <= 0.0f))
{
return true;
}
}
return false;
}
bool HitTester::testMesh(
const IAABB& area,
Span<Vec2D> verts,
Span<uint16_t> indices)
{
// this version can give slightly different results, so perhaps we should do
// this automatically, ... its just much faster if we do.
if (area.width() * area.height() == 1)
{
return testMesh(Vec2D((float)area.left, (float)area.top), verts, indices);
}
if (verts.size() < 3)
{
return false;
}
// Test against the bounds of the entire mesh
// Make this optional?
if (CULL_BOUNDS)
{
const auto bounds = AABB(verts);
if (bounds.bottom() <= area.top || area.bottom <= bounds.top() ||
bounds.right() <= area.left || area.right <= bounds.left())
{
return false;
}
}
std::vector<int> windings(area.width() * area.height());
const auto offset = Vec2D((float)area.left, (float)area.top);
int* deltas = windings.data();
for (size_t i = 0; i < indices.size(); i += 3)
{
const auto a = verts[indices[i + 0]] - offset;
const auto b = verts[indices[i + 1]] - offset;
const auto c = verts[indices[i + 2]] - offset;
clip_line((float)area.height(), a, b, deltas, area.width());
clip_line((float)area.height(), b, c, deltas, area.width());
clip_line((float)area.height(), c, a, deltas, area.width());
int nonzero = 0;
for (auto w : windings)
{
nonzero |= w;
}
if (nonzero)
{
return true;
}
}
return false;
}
// Barycentric weights of `p` in triangle abc; false when it's degenerate.
// From cross products in doubles: the Gram determinant squares the area,
// which a long, thin triangle then loses in float.
static bool barycentric(Vec2D p, Vec2D a, Vec2D b, Vec2D c, float weights[3])
{
const double abx = (double)b.x - a.x;
const double aby = (double)b.y - a.y;
const double acx = (double)c.x - a.x;
const double acy = (double)c.y - a.y;
const double apx = (double)p.x - a.x;
const double apy = (double)p.y - a.y;
const double area = abx * acy - aby * acx;
if (!(std::abs(area) >
1e-6 * std::sqrt((abx * abx + aby * aby) * (acx * acx + acy * acy))))
{
return false;
}
const double wb = (apx * acy - apy * acx) / area;
const double wc = (abx * apy - aby * apx) / area;
weights[0] = (float)(1.0 - wb - wc);
weights[1] = (float)wb;
weights[2] = (float)wc;
return true;
}
// The affine map taking triangle abc onto ABC, which barycentric found
// isn't degenerate.
static Mat2D triangleMap(Vec2D a, Vec2D b, Vec2D c, Vec2D A, Vec2D B, Vec2D C)
{
const double abx = (double)b.x - a.x;
const double aby = (double)b.y - a.y;
const double acx = (double)c.x - a.x;
const double acy = (double)c.y - a.y;
const double area = abx * acy - aby * acx;
// How the weights of B and C change with x and with y.
const double bx = acy / area;
const double by = -acx / area;
const double cx = -aby / area;
const double cy = abx / area;
const double ABx = (double)B.x - A.x;
const double ABy = (double)B.y - A.y;
const double ACx = (double)C.x - A.x;
const double ACy = (double)C.y - A.y;
const double xx = ABx * bx + ACx * cx;
const double xy = ABy * bx + ACy * cx;
const double yx = ABx * by + ACx * cy;
const double yy = ABy * by + ACy * cy;
return Mat2D(
(float)xx,
(float)xy,
(float)yx,
(float)yy,
(float)(A.x - (xx * a.x + yx * a.y)),
(float)(A.y - (xy * a.x + yy * a.y)));
}
static float segmentDistanceSquared(Vec2D p, Vec2D a, Vec2D b)
{
const Vec2D ab = b - a;
const float lengthSquared = ab.lengthSquared();
const float t = lengthSquared > 0.0f ? Vec2D::dot(p - a, ab) / lengthSquared
: 0.0f;
return Vec2D::distanceSquared(p, a + ab * std::min(std::max(t, 0.0f), 1.0f));
}
bool HitTester::mapMesh(
Vec2D point,
Span<const Vec2D> from,
Span<const Vec2D> to,
Span<const uint16_t> indices,
Vec2D* out,
bool* inside,
Mat2D* map)
{
auto land = [&](const uint16_t triangle[3], const float weights[3]) {
*out = to[triangle[0]] * weights[0] + to[triangle[1]] * weights[1] +
to[triangle[2]] * weights[2];
if (map != nullptr)
{
*map = triangleMap(
from[triangle[0]],
from[triangle[1]],
from[triangle[2]],
to[triangle[0]],
to[triangle[1]],
to[triangle[2]]);
}
};
// Lets a point on the seam between two triangles land in one of them.
constexpr float edgeTolerance = 1e-5f;
bool hasNearest = false;
float nearestDistance = 0.0f;
uint16_t nearest[3] = {0, 0, 0};
float nearestWeights[3] = {0.0f, 0.0f, 0.0f};
// Later triangles draw over earlier ones, so walk back to find the top one.
for (size_t i = indices.size() - indices.size() % 3; i >= 3; i -= 3)
{
const uint16_t triangle[3] = {
indices[i - 3],
indices[i - 2],
indices[i - 1]};
bool inRange = true;
for (auto index : triangle)
{
inRange = inRange && index < from.size() && index < to.size();
}
float weights[3];
if (!inRange || !barycentric(
point,
from[triangle[0]],
from[triangle[1]],
from[triangle[2]],
weights))
{
continue;
}
if (weights[0] >= -edgeTolerance && weights[1] >= -edgeTolerance &&
weights[2] >= -edgeTolerance)
{
land(triangle, weights);
*inside = true;
return true;
}
const float distance = std::min(
{segmentDistanceSquared(point, from[triangle[0]], from[triangle[1]]),
segmentDistanceSquared(point, from[triangle[1]], from[triangle[2]]),
segmentDistanceSquared(point, from[triangle[2]], from[triangle[0]])});
if (!hasNearest || distance < nearestDistance)
{
hasNearest = true;
nearestDistance = distance;
std::copy(triangle, triangle + 3, nearest);
std::copy(weights, weights + 3, nearestWeights);
}
}
if (!hasNearest)
{
return false;
}
// Off the mesh: extend the nearest triangle past its edges, which keeps
// the mapping continuous as the point crosses them.
land(nearest, nearestWeights);
*inside = false;
return true;
}