blob: ad1ecc8da9bb719c12d9d6bc64ab266ee4d97f15 [file]
#include "rive/text/text_selection_path.hpp"
#include "rive/shapes/path.hpp"
using namespace rive;
// Crossing-number test of a point against a simple closed contour. The
// contours coming out of RectanglesToContour never touch each other, so
// probing with a vertex of another contour is safe.
static bool contourContains(const Contour& contour, Vec2D point)
{
size_t size = contour.size();
if (size < 3)
{
return false;
}
bool inside = false;
for (size_t i = 0, j = size - 1; i < size; j = i++)
{
Vec2D a = contour.point(i);
Vec2D b = contour.point(j);
if ((a.y > point.y) != (b.y > point.y) &&
point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x)
{
inside = !inside;
}
}
return inside;
}
void TextSelectionPath::update(Span<AABB> rects, float cornerRadius)
{
rewind();
m_rectanglesToContour.reset();
for (const AABB& rect : rects)
{
m_rectanglesToContour.addRect(rect);
}
m_rectanglesToContour.computeContours();
RawPath& rawPath = *mutableRawPath();
size_t count = m_rectanglesToContour.contourCount();
for (size_t i = 0; i < count; i++)
{
Contour contour = m_rectanglesToContour.contour(i);
if (contour.size() < 2)
{
continue;
}
// Wind outer contours clockwise and holes counter-clockwise so the
// geometry reads the same under the clockwise, non-zero and even-odd
// fill rules. Feathered fills are only drawn when the path is
// clockwise, so this is what lets a feather apply here at all.
Vec2D probe = contour.point(0);
size_t depth = 0;
for (size_t j = 0; j < count; j++)
{
if (j != i &&
contourContains(m_rectanglesToContour.contour(j), probe))
{
depth++;
}
}
addRoundedPath(contour, cornerRadius, rawPath, (depth & 1) == 0);
}
}
void TextSelectionPath::addRoundedPath(const Contour& contour,
float radius,
RawPath& rawPath,
bool clockwise)
{
bool reversed = contour.isClockwise() != clockwise;
size_t length = contour.size();
if (length < 2)
{
return;
}
Vec2D firstPoint = contour.point(0, reversed);
Vec2D point = firstPoint;
if (radius > 0.0f)
{
Vec2D prev = contour.point(length - 1, reversed);
Vec2D pos = point;
Vec2D toPrev = prev - pos;
float toPrevLength = toPrev.length();
toPrev.x /= toPrevLength;
toPrev.y /= toPrevLength;
Vec2D next = contour.point(1, reversed);
Vec2D toNext = next - pos;
float toNextLength = toNext.length();
toNext.x /= toNextLength;
toNext.y /= toNextLength;
float renderRadius = std::min(toPrevLength / 2.0f,
std::min(toNextLength / 2.0f, radius));
float idealDistance =
Path::computeIdealControlPointDistance(toPrev,
toNext,
renderRadius);
Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
rawPath.moveTo(translation.x, translation.y);
Vec2D outPoint =
Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
Vec2D inPoint =
Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
Vec2D posNext = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
rawPath.cubicTo(outPoint.x,
outPoint.y,
inPoint.x,
inPoint.y,
posNext.x,
posNext.y);
}
else
{
rawPath.moveTo(point.x, point.y);
}
for (size_t i = 1; i < length; i++)
{
Vec2D point = contour.point(i, reversed);
if (radius > 0.0f)
{
Vec2D prev = contour.point(i - 1, reversed);
Vec2D pos = point;
Vec2D toPrev = prev - pos;
float toPrevLength = toPrev.length();
toPrev.x /= toPrevLength;
toPrev.y /= toPrevLength;
Vec2D next = contour.point((i + 1) % length, reversed);
Vec2D toNext = next - pos;
float toNextLength = toNext.length();
toNext.x /= toNextLength;
toNext.y /= toNextLength;
float renderRadius =
std::min(toPrevLength / 2.0f,
std::min(toNextLength / 2.0f, radius));
float idealDistance =
Path::computeIdealControlPointDistance(toPrev,
toNext,
renderRadius);
Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
rawPath.lineTo(translation.x, translation.y);
Vec2D outPoint =
Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
Vec2D inPoint =
Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
Vec2D posNext = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
rawPath.cubicTo(outPoint.x,
outPoint.y,
inPoint.x,
inPoint.y,
posNext.x,
posNext.y);
}
else
{
rawPath.lineTo(point.x, point.y);
}
}
rawPath.close();
}