| /* |
| * Copyright 2022 Rive |
| */ |
| |
| #include "rive/math/raw_path.hpp" |
| |
| #include "rive/command_path.hpp" |
| #include "rive/math/simd.hpp" |
| #include "rive/math/wangs_formula.hpp" |
| #include "rive/math/bezier_utils.hpp" |
| #include <cmath> |
| #include <cstring> |
| #include <algorithm> |
| |
| namespace rive |
| { |
| bool RawPath::operator==(const RawPath& o) const |
| { |
| return m_Points == o.m_Points && m_Verbs == o.m_Verbs; |
| } |
| |
| AABB RawPath::bounds() const |
| { |
| float4 mins, maxes; |
| size_t i; |
| if (m_Points.size() & 1) |
| { |
| mins = maxes = simd::load2f(&m_Points[0].x).xyxy; |
| i = 1; |
| } |
| else |
| { |
| mins = maxes = m_Points.empty() ? float4{0, 0, 0, 0} |
| : simd::load4f(&m_Points[0].x); |
| i = 2; |
| } |
| for (; i < m_Points.size(); i += 2) |
| { |
| float4 pts = simd::load4f(&m_Points[i].x); |
| mins = simd::min(mins, pts); |
| maxes = simd::max(maxes, pts); |
| } |
| AABB bounds; |
| simd::store(&bounds.minX, simd::min(mins.xy, mins.zw)); |
| simd::store(&bounds.maxX, simd::max(maxes.xy, maxes.zw)); |
| return bounds; |
| } |
| |
| size_t RawPath::countMoveTos() const |
| { |
| size_t moveToCount = 0; |
| for (PathVerb verb : m_Verbs) |
| { |
| moveToCount += verb == PathVerb::move ? 1 : 0; |
| } |
| return moveToCount; |
| } |
| |
| void RawPath::injectImplicitMoveIfNeeded() |
| { |
| if (!m_contourIsOpen) |
| { |
| move(m_Points.empty() ? Vec2D{0, 0} : m_Points[m_lastMoveIdx]); |
| } |
| } |
| |
| void RawPath::move(Vec2D a) |
| { |
| m_contourIsOpen = true; |
| m_lastMoveIdx = m_Points.size(); |
| m_Points.push_back(a); |
| m_Verbs.push_back(PathVerb::move); |
| } |
| |
| void RawPath::line(Vec2D a) |
| { |
| injectImplicitMoveIfNeeded(); |
| m_Points.push_back(a); |
| m_Verbs.push_back(PathVerb::line); |
| } |
| |
| void RawPath::quad(Vec2D a, Vec2D b) |
| { |
| injectImplicitMoveIfNeeded(); |
| m_Points.push_back(a); |
| m_Points.push_back(b); |
| m_Verbs.push_back(PathVerb::quad); |
| } |
| |
| void RawPath::cubic(Vec2D a, Vec2D b, Vec2D c) |
| { |
| injectImplicitMoveIfNeeded(); |
| m_Points.push_back(a); |
| m_Points.push_back(b); |
| m_Points.push_back(c); |
| m_Verbs.push_back(PathVerb::cubic); |
| } |
| |
| void RawPath::close() |
| { |
| if (m_contourIsOpen) |
| { |
| m_Verbs.push_back(PathVerb::close); |
| m_contourIsOpen = false; |
| } |
| } |
| |
| bool RawPath::isClosed() const |
| { |
| if (m_Verbs.size() > 0) |
| { |
| return m_Verbs[m_Verbs.size() - 1] == PathVerb::close; |
| } |
| return false; |
| } |
| |
| RawPath RawPath::transform(const Mat2D& m) const |
| { |
| RawPath path; |
| |
| path.m_Verbs = m_Verbs; |
| |
| path.m_Points.resize(m_Points.size()); |
| m.mapPoints(path.m_Points.data(), m_Points.data(), m_Points.size()); |
| return path; |
| } |
| |
| void RawPath::transformInPlace(const Mat2D& m) |
| { |
| m.mapPoints(m_Points.data(), m_Points.data(), m_Points.size()); |
| } |
| |
| void RawPath::addRect(const AABB& r, PathDirection dir) |
| { |
| // We manually close the rectangle, in case we want to stroke |
| // this path. We also call close() so we get proper joins |
| // (and not caps). |
| |
| m_Points.reserve(5); |
| m_Verbs.reserve(6); |
| |
| moveTo(r.left(), r.top()); |
| if (dir == PathDirection::clockwise) |
| { |
| lineTo(r.right(), r.top()); |
| lineTo(r.right(), r.bottom()); |
| lineTo(r.left(), r.bottom()); |
| } |
| else |
| { |
| lineTo(r.left(), r.bottom()); |
| lineTo(r.right(), r.bottom()); |
| lineTo(r.right(), r.top()); |
| } |
| close(); |
| } |
| |
| void RawPath::addOval(const AABB& r, PathDirection dir) |
| { |
| // see https://spencermortensen.com/articles/bezier-circle/ |
| constexpr float C = 0.5519150244935105707435627f; |
| // precompute clockwise unit circle, starting and ending at {1, 0} |
| constexpr rive::Vec2D unit[] = { |
| {1, 0}, |
| {1, C}, |
| {C, 1}, // quadrant 1 ( 4:30) |
| {0, 1}, |
| {-C, 1}, |
| {-1, C}, // quadrant 2 ( 7:30) |
| {-1, 0}, |
| {-1, -C}, |
| {-C, -1}, // quadrant 3 (10:30) |
| {0, -1}, |
| {C, -1}, |
| {1, -C}, // quadrant 4 ( 1:30) |
| {1, 0}, |
| }; |
| |
| const auto center = r.center(); |
| const float dx = center.x; |
| const float dy = center.y; |
| const float sx = r.width() * 0.5f; |
| const float sy = r.height() * 0.5f; |
| |
| auto map = [dx, dy, sx, sy](rive::Vec2D p) { |
| return rive::Vec2D(p.x * sx + dx, p.y * sy + dy); |
| }; |
| |
| m_Points.reserve(13); |
| m_Verbs.reserve(6); |
| |
| if (dir == PathDirection::clockwise) |
| { |
| move(map(unit[0])); |
| for (int i = 1; i <= 12; i += 3) |
| { |
| cubic(map(unit[i + 0]), map(unit[i + 1]), map(unit[i + 2])); |
| } |
| } |
| else |
| { |
| move(map(unit[12])); |
| for (int i = 11; i >= 0; i -= 3) |
| { |
| cubic(map(unit[i - 0]), map(unit[i - 1]), map(unit[i - 2])); |
| } |
| } |
| close(); |
| } |
| |
| void RawPath::addPoly(Span<const Vec2D> span, bool isClosed) |
| { |
| if (span.size() == 0) |
| { |
| return; |
| } |
| |
| // should we permit must moveTo() or just moveTo()/close() ? |
| |
| m_Points.reserve(span.size() + isClosed); |
| m_Verbs.reserve(span.size() + isClosed); |
| |
| move(span[0]); |
| for (size_t i = 1; i < span.size(); ++i) |
| { |
| line(span[i]); |
| } |
| if (isClosed) |
| { |
| close(); |
| } |
| } |
| |
| void RawPath::addPoints(std::vector<Vec2D>::const_iterator& ptIter, |
| int count, |
| const Mat2D* mat) |
| { |
| while (count > 0) |
| { |
| auto point = *ptIter; |
| if (mat) |
| { |
| Vec2D transformedPoint = Vec2D::transformMat2D(point, *mat); |
| m_Points.emplace_back(transformedPoint); |
| } |
| else |
| { |
| m_Points.emplace_back(point); |
| } |
| ptIter--; |
| count--; |
| } |
| } |
| |
| RawPath::Iter RawPath::addPath(const RawPath& src, const Mat2D* mat) |
| { |
| size_t initialVerbCount = m_Verbs.size(); |
| size_t initialPointCount = m_Points.size(); |
| |
| m_Verbs.insert(m_Verbs.end(), src.m_Verbs.cbegin(), src.m_Verbs.cend()); |
| |
| if (mat) |
| { |
| const auto oldPointCount = m_Points.size(); |
| m_Points.resize(oldPointCount + src.m_Points.size()); |
| Vec2D* dst = m_Points.data() + oldPointCount; |
| mat->mapPoints(dst, src.m_Points.data(), src.m_Points.size()); |
| } |
| else |
| { |
| m_Points.insert(m_Points.end(), |
| src.m_Points.cbegin(), |
| src.m_Points.cend()); |
| } |
| |
| // Return an iterator at the beginning of the newly added geometry. |
| return Iter{m_Verbs.data() + initialVerbCount, |
| m_Points.data() + initialPointCount}; |
| } |
| |
| RawPath::Iter RawPath::addPathBackwards(const RawPath& src, const Mat2D* mat) |
| { |
| size_t initialVerbCount = m_Verbs.size(); |
| size_t initialPointCount = m_Points.size(); |
| if (!src.m_Verbs.empty()) |
| { |
| bool isClosed = src.m_Verbs.back() == PathVerb::close; |
| |
| auto reversePointIterator = src.m_Points.end() - 1; |
| // Move to first point |
| m_Verbs.emplace_back(PathVerb::move); |
| addPoints(reversePointIterator, 1, mat); |
| |
| auto reverseVerbIterator = src.m_Verbs.end() - 1; |
| if (isClosed) |
| { |
| reverseVerbIterator--; |
| } |
| |
| bool hasPendingMoveCommand = false; |
| |
| for (; reverseVerbIterator != src.m_Verbs.begin(); |
| reverseVerbIterator--) |
| { |
| PathVerb verb = *reverseVerbIterator; |
| // For paths that have multiple segments (like in certain font |
| // glyphs), we need to flip the intermediate move verbs with the |
| // previous close path. So we hold on to it and add it after the |
| // next close call. |
| if (verb == PathVerb::move) |
| { |
| hasPendingMoveCommand = true; |
| } |
| else |
| { |
| if (hasPendingMoveCommand && verb != PathVerb::close) |
| { |
| m_Verbs.emplace_back(PathVerb::move); |
| hasPendingMoveCommand = false; |
| } |
| m_Verbs.emplace_back(verb); |
| if (hasPendingMoveCommand && verb == PathVerb::close) |
| { |
| m_Verbs.emplace_back(PathVerb::move); |
| hasPendingMoveCommand = false; |
| } |
| } |
| switch (verb) |
| { |
| case PathVerb::cubic: |
| addPoints(reversePointIterator, 3, mat); |
| break; |
| case PathVerb::quad: |
| addPoints(reversePointIterator, 2, mat); |
| break; |
| case PathVerb::line: |
| case PathVerb::move: |
| addPoints(reversePointIterator, 1, mat); |
| break; |
| default: |
| break; |
| } |
| } |
| // This should never be the case, but if for some reason a path ends |
| // with a move command, we add it to the list of verbs to ensure the |
| // path matches with the number of points |
| if (hasPendingMoveCommand) |
| { |
| m_Verbs.emplace_back(PathVerb::move); |
| } |
| m_Verbs.emplace_back(PathVerb::close); |
| } |
| return Iter{m_Verbs.data() + initialVerbCount, |
| m_Points.data() + initialPointCount}; |
| } |
| |
| void RawPath::pruneEmptySegments(Iter start) |
| { |
| auto dstVerb = |
| m_Verbs.begin() + |
| std::distance<const PathVerb*>(m_Verbs.data(), start.rawVerbsPtr()); |
| auto dstPts = |
| m_Points.begin() + |
| std::distance<const Vec2D*>(m_Points.data(), start.rawPtsPtr()); |
| decltype(m_Verbs)::const_iterator srcVerb = dstVerb; |
| decltype(m_Points)::const_iterator srcPts = dstPts; |
| |
| int ptsAdvance; |
| for (auto end = m_Verbs.end(); srcVerb != end; |
| ++srcVerb, srcPts += ptsAdvance) |
| { |
| PathVerb verb = *srcVerb; |
| ptsAdvance = Iter::PtsAdvanceAfterVerb(verb); |
| |
| switch (verb) |
| { |
| case PathVerb::move: |
| break; |
| case PathVerb::close: |
| break; |
| case PathVerb::cubic: |
| if (srcPts[2] != srcPts[1]) |
| { |
| break; |
| } |
| RIVE_FALLTHROUGH; |
| case PathVerb::quad: |
| if (srcPts[1] != srcPts[0]) |
| { |
| break; |
| } |
| RIVE_FALLTHROUGH; |
| case PathVerb::line: |
| if (srcPts[0] != srcPts[-1]) |
| { |
| break; |
| } |
| // This segment is empty! Don't keep it. |
| continue; |
| } |
| |
| if (srcVerb != dstVerb) |
| { |
| *dstVerb = verb; |
| std::copy(srcPts, srcPts + ptsAdvance, dstPts); |
| } |
| |
| ++dstVerb; |
| dstPts += ptsAdvance; |
| } |
| |
| if (dstVerb != srcVerb) |
| { |
| m_Verbs.erase(dstVerb, m_Verbs.end()); |
| m_Points.erase(dstPts, m_Points.end()); |
| } |
| } |
| |
| ////////////////////////////////////////////////////////////////////////// |
| int path_verb_to_point_count(PathVerb v) |
| { |
| static uint8_t ptCounts[] = { |
| 1, // move |
| 1, // line |
| 2, // quad |
| 2, // conic (unused) |
| 3, // cubic |
| 0, // close |
| }; |
| size_t index = (size_t)v; |
| assert(index < sizeof(ptCounts)); |
| return ptCounts[index]; |
| } |
| |
| void RawPath::swap(RawPath& rawPath) |
| { |
| m_Points.swap(rawPath.m_Points); |
| m_Verbs.swap(rawPath.m_Verbs); |
| } |
| |
| void RawPath::reset() |
| { |
| m_Points.clear(); |
| m_Points.shrink_to_fit(); |
| m_Verbs.clear(); |
| m_Verbs.shrink_to_fit(); |
| m_contourIsOpen = false; |
| } |
| |
| void RawPath::rewind() |
| { |
| m_Points.clear(); |
| m_Verbs.clear(); |
| m_contourIsOpen = false; |
| } |
| |
| /////////////////////////////////// |
| |
| void RawPath::addTo(CommandPath* result) const |
| { |
| for (auto iter : *this) |
| { |
| PathVerb verb = std::get<0>(iter); |
| const Vec2D* pts = std::get<1>(iter); |
| switch (verb) |
| { |
| case PathVerb::move: |
| result->move(pts[0]); |
| break; |
| case PathVerb::line: |
| result->line(pts[1]); |
| break; |
| case PathVerb::cubic: |
| result->cubic(pts[1], pts[2], pts[3]); |
| break; |
| case PathVerb::close: |
| result->close(); |
| break; |
| case PathVerb::quad: |
| // TODO: actually supports quads. |
| result->cubic(Vec2D::lerp(pts[0], pts[1], 2 / 3.f), |
| Vec2D::lerp(pts[2], pts[1], 2 / 3.f), |
| pts[2]); |
| } |
| } |
| } |
| |
| void RawPath::quadToCubic(float x, float y, float x1, float y1) |
| { |
| assert(!m_Points.empty()); |
| if (m_Points.empty()) |
| { |
| return; |
| } |
| const Vec2D& p0 = m_Points.back(); |
| const Vec2D p1(x, y); |
| const Vec2D p2(x1, y1); |
| cubic(Vec2D::lerp(p0, p1, 2 / 3.f), Vec2D::lerp(p2, p1, 2 / 3.f), p2); |
| } |
| |
| #ifdef DEBUG |
| void RawPath::printCode() const |
| { |
| fprintf(stderr, "RawPath path;\n"); |
| for (auto iter : *this) |
| { |
| PathVerb verb = std::get<0>(iter); |
| const Vec2D* pts = std::get<1>(iter); |
| switch (verb) |
| { |
| case PathVerb::move: |
| fprintf(stderr, "path.moveTo(%f, %f);\n", pts[0].x, pts[0].y); |
| break; |
| case PathVerb::line: |
| fprintf(stderr, "path.lineTo(%f, %f);\n", pts[1].x, pts[1].y); |
| break; |
| case PathVerb::cubic: |
| fprintf(stderr, |
| "path.cubicTo(%f, %f, %f, %f, %f, %f);\n", |
| pts[1].x, |
| pts[1].y, |
| pts[2].x, |
| pts[2].y, |
| pts[3].x, |
| pts[3].y); |
| break; |
| case PathVerb::close: |
| fprintf(stderr, "path.close();\n"); |
| break; |
| case PathVerb::quad: |
| fprintf(stderr, |
| "path.quadTo(%f, %f, %f, %f);\n", |
| pts[1].x, |
| pts[1].y, |
| pts[2].x, |
| pts[2].y); |
| } |
| } |
| fprintf(stderr, "\n"); |
| } |
| #endif |
| #ifdef WITH_RIVE_TOOLS |
| static void expandAxisBounds(AABB& bounds, int axis, float value) |
| { |
| switch (axis) |
| { |
| case 0: |
| if (value < bounds.minX) |
| { |
| bounds.minX = value; |
| } |
| if (value > bounds.maxX) |
| { |
| bounds.maxX = value; |
| } |
| break; |
| case 1: |
| if (value < bounds.minY) |
| { |
| bounds.minY = value; |
| } |
| if (value > bounds.maxY) |
| { |
| bounds.maxY = value; |
| } |
| break; |
| default: |
| RIVE_UNREACHABLE(); |
| } |
| } |
| |
| // Expand our bounds to a point (in normalized T space) on the cubic. |
| static void expandBoundsToCubicPoint(AABB& bounds, |
| int axis, |
| float t, |
| float a, |
| float b, |
| float c, |
| float d) |
| { |
| if (t >= 0.0f && t <= 1.0f) |
| { |
| float ti = 1.0f - t; |
| float pointAtT = ((ti * ti * ti) * a) + ((3.0f * ti * ti * t) * b) + |
| ((3.0f * ti * t * t) * c) + (t * t * t * d); |
| expandAxisBounds(bounds, axis, pointAtT); |
| } |
| } |
| |
| // Based on finding the extremas of the curve as described here: |
| // https://pomax.github.io/bezierinfo/#extremities and based on PR we provided |
| // to the Flutter team here: https://github.com/flutter/engine/pull/19054 and |
| // here: |
| // https://github.com/luigi-rosso/engine/blob/9ae3efd7dc6bcb9634402b4b8818d0add096c12d/lib/web_ui/lib/src/engine/surface/path.dart#L892 |
| static void expandCubicBoundsForAxis(AABB& bounds, |
| int axis, |
| float start, |
| float cp1, |
| float cp2, |
| float end) |
| { |
| // Check start/end as cubic goes through those. |
| expandAxisBounds(bounds, axis, start); |
| expandAxisBounds(bounds, axis, end); |
| // Now check extremas. |
| |
| // Find the first derivative |
| float a = 3.0f * (cp1 - start); |
| float b = 3.0f * (cp2 - cp1); |
| float c = 3.0f * (end - cp2); |
| float d = a - 2.0f * b + c; |
| |
| // Solve roots for first derivative. |
| if (d != 0) |
| { |
| float m1 = -sqrtf(b * b - a * c); |
| float m2 = -a + b; |
| |
| // First root. |
| expandBoundsToCubicPoint(bounds, |
| axis, |
| -(m1 + m2) / d, |
| start, |
| cp1, |
| cp2, |
| end); |
| expandBoundsToCubicPoint(bounds, |
| axis, |
| -(-m1 + m2) / d, |
| start, |
| cp1, |
| cp2, |
| end); |
| } |
| else if (b != c && d == 0) |
| { |
| expandBoundsToCubicPoint(bounds, |
| axis, |
| (2.0f * b - c) / (2.0f * (b - c)), |
| start, |
| cp1, |
| cp2, |
| end); |
| } |
| |
| // Derive the first derivative to get the 2nd and use the root of |
| // that (linear). |
| float d2a = 2.0f * (b - a); |
| float d2b = 2.0f * (c - b); |
| if (d2a != b) |
| { |
| expandBoundsToCubicPoint(bounds, |
| axis, |
| d2a / (d2a - d2b), |
| start, |
| cp1, |
| cp2, |
| end); |
| } |
| } |
| |
| AABB RawPath::preciseBounds() const |
| { |
| AABB bounds = AABB::forExpansion(); |
| for (auto iter : *this) |
| { |
| PathVerb verb = std::get<0>(iter); |
| const Vec2D* pts = std::get<1>(iter); |
| switch (verb) |
| { |
| case PathVerb::move: |
| bounds.expandTo(bounds, pts[0]); |
| break; |
| case PathVerb::line: |
| bounds.expandTo(bounds, pts[1]); |
| break; |
| case PathVerb::cubic: |
| expandCubicBoundsForAxis(bounds, |
| 0, |
| pts[0].x, |
| pts[1].x, |
| pts[2].x, |
| pts[3].x); |
| expandCubicBoundsForAxis(bounds, |
| 1, |
| pts[0].y, |
| pts[1].y, |
| pts[2].y, |
| pts[3].y); |
| break; |
| case PathVerb::close: |
| break; |
| case PathVerb::quad: |
| // Rive very rarely computes precise bounds for quadratics so we |
| // don't implement this specific case. We do use it in the |
| // editor for some cases so we still solve it as a cubic. |
| Vec2D pt1 = Vec2D::lerp(pts[0], pts[1], 2 / 3.f); |
| Vec2D pt2 = Vec2D::lerp(pts[2], pts[1], 2 / 3.f); |
| expandCubicBoundsForAxis(bounds, |
| 0, |
| pts[0].x, |
| pt1.x, |
| pt2.x, |
| pts[2].x); |
| expandCubicBoundsForAxis(bounds, |
| 1, |
| pts[0].y, |
| pt1.y, |
| pt2.y, |
| pts[2].y); |
| break; |
| } |
| } |
| return bounds; |
| } |
| #endif |
| |
| float RawPath::computeCoarseArea() const |
| { |
| float a = 0; |
| Vec2D contourP0 = {0, 0}, lastPt = {0, 0}; |
| for (auto iter : *this) |
| { |
| PathVerb verb = std::get<0>(iter); |
| const Vec2D* pts = std::get<1>(iter); |
| switch (verb) |
| { |
| case PathVerb::move: |
| a += Vec2D::cross(lastPt, contourP0); |
| contourP0 = lastPt = pts[0]; |
| break; |
| case PathVerb::close: |
| break; |
| case PathVerb::line: |
| a += Vec2D::cross(lastPt, pts[1]); |
| lastPt = pts[1]; |
| break; |
| case PathVerb::quad: |
| RIVE_UNREACHABLE(); |
| case PathVerb::cubic: |
| { |
| // Linearize the cubic in artboard space, then add up the |
| // area for each segment. |
| float n = ceilf( |
| wangs_formula::cubic(pts, 1.f / kCoarseAreaTolerance)); |
| if (n > 1) |
| { |
| n = std::min(n, 64.f); |
| float4 t = float4{1, 1, 2, 2} * (1 / n); |
| float4 dt = t.w; |
| math::EvalCubic evalCubic(pts); |
| for (; t.x < 1; t += dt) |
| { |
| float4 p = evalCubic(t); |
| Vec2D lo = {p.x, p.y}; |
| a += Vec2D::cross(lastPt, lo); |
| lastPt = lo; |
| if (t.y < 1) |
| { |
| Vec2D hi = {p.z, p.w}; |
| a += Vec2D::cross(lastPt, hi); |
| lastPt = hi; |
| } |
| } |
| } |
| a += Vec2D::cross(lastPt, pts[3]); |
| lastPt = pts[3]; |
| break; |
| } |
| } |
| } |
| a += Vec2D::cross(lastPt, contourP0); |
| return a * .5f; |
| } |
| } // namespace rive |