| #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(); |
| } |