| #include "rive/shapes/path.hpp" | |
| #include "rive/renderer.hpp" | |
| #include "rive/shapes/cubic_vertex.hpp" | |
| #include "rive/shapes/cubic_detached_vertex.hpp" | |
| #include "rive/shapes/path_vertex.hpp" | |
| #include "rive/shapes/shape.hpp" | |
| #include "rive/shapes/straight_vertex.hpp" | |
| #include "rive/math/math_types.hpp" | |
| #include <cassert> | |
| using namespace rive; | |
| /// Compute an ideal control point distance to create a curve of the given | |
| /// radius. Based on "natural rounding" https://observablehq.com/@daformat/rounding-polygon-corners | |
| static float computeIdealControlPointDistance(const Vec2D& toPrev, | |
| const Vec2D& toNext, | |
| float radius) | |
| { | |
| // Get the angle between next and prev | |
| float angle = fabs(atan2(Vec2D::cross(toPrev, toNext), Vec2D::dot(toPrev, toNext))); | |
| return fmin(radius, | |
| (4.0f / 3.0f) * tan(math::PI / (2.0f * ((2.0f * math::PI) / angle))) * radius * | |
| (angle < math::PI / 2 ? 1 + cos(angle) : 2.0f - sin(angle))); | |
| } | |
| StatusCode Path::onAddedClean(CoreContext* context) | |
| { | |
| StatusCode code = Super::onAddedClean(context); | |
| if (code != StatusCode::Ok) | |
| { | |
| return code; | |
| } | |
| // Find the shape. | |
| for (auto currentParent = parent(); currentParent != nullptr; | |
| currentParent = currentParent->parent()) | |
| { | |
| if (currentParent->is<Shape>()) | |
| { | |
| m_Shape = currentParent->as<Shape>(); | |
| m_Shape->addPath(this); | |
| return StatusCode::Ok; | |
| } | |
| } | |
| return StatusCode::MissingObject; | |
| } | |
| void Path::buildDependencies() { Super::buildDependencies(); } | |
| void Path::addVertex(PathVertex* vertex) { m_Vertices.push_back(vertex); } | |
| void Path::addFlags(PathFlags flags) { m_pathFlags |= flags; } | |
| bool Path::isFlagged(PathFlags flags) const { return (int)(m_pathFlags & flags) != 0x00; } | |
| bool Path::canDeferPathUpdate() | |
| { | |
| if (m_Shape == nullptr) | |
| { | |
| return false; | |
| } | |
| // A path cannot defer its update if the shapes requires an update. Note the | |
| // nuance here where we track that the shape may be marked for follow path | |
| // (meaning all child paths need to follow path). This doesn't mean the | |
| // Shape is necessarily forced to update put the paths are, which is why we | |
| // explicitly also check the shape's path space. | |
| return m_Shape->canDeferPathUpdate() && !m_Shape->isFlagged(PathFlags::followPath) && | |
| !isFlagged(PathFlags::followPath | PathFlags::clipping); | |
| } | |
| const Mat2D& Path::pathTransform() const { return worldTransform(); } | |
| void Path::buildPath(RawPath& rawPath) const | |
| { | |
| const bool isClosed = isPathClosed(); | |
| const std::vector<PathVertex*>& vertices = m_Vertices; | |
| auto length = vertices.size(); | |
| if (length < 2) | |
| { | |
| return; | |
| } | |
| auto firstPoint = vertices[0]; | |
| // Init out to translation | |
| Vec2D out; | |
| bool prevIsCubic; | |
| Vec2D start, startIn; | |
| bool startIsCubic; | |
| if (firstPoint->is<CubicVertex>()) | |
| { | |
| auto cubic = firstPoint->as<CubicVertex>(); | |
| startIsCubic = prevIsCubic = true; | |
| startIn = cubic->renderIn(); | |
| out = cubic->renderOut(); | |
| start = cubic->renderTranslation(); | |
| rawPath.move(start); | |
| } | |
| else | |
| { | |
| startIsCubic = prevIsCubic = false; | |
| auto point = *firstPoint->as<StraightVertex>(); | |
| auto radius = point.radius(); | |
| if (radius > 0.0f) | |
| { | |
| auto prev = vertices[length - 1]; | |
| Vec2D pos = point.renderTranslation(); | |
| Vec2D toPrev = (prev->is<CubicVertex>() ? prev->as<CubicVertex>()->renderOut() | |
| : prev->renderTranslation()) - | |
| pos; | |
| auto toPrevLength = toPrev.normalizeLength(); | |
| auto next = vertices[1]; | |
| Vec2D toNext = (next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn() | |
| : next->renderTranslation()) - | |
| pos; | |
| auto toNextLength = toNext.normalizeLength(); | |
| float renderRadius = | |
| std::min(toPrevLength / 2.0f, std::min(toNextLength / 2.0f, radius)); | |
| float idealDistance = computeIdealControlPointDistance(toPrev, toNext, renderRadius); | |
| startIn = start = Vec2D::scaleAndAdd(pos, toPrev, renderRadius); | |
| rawPath.move(startIn); | |
| Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance); | |
| Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance); | |
| out = Vec2D::scaleAndAdd(pos, toNext, renderRadius); | |
| rawPath.cubic(outPoint, inPoint, out); | |
| prevIsCubic = false; | |
| } | |
| else | |
| { | |
| startIn = start = out = point.renderTranslation(); | |
| rawPath.move(out); | |
| } | |
| } | |
| for (size_t i = 1; i < length; i++) | |
| { | |
| auto vertex = vertices[i]; | |
| if (vertex->is<CubicVertex>()) | |
| { | |
| auto cubic = vertex->as<CubicVertex>(); | |
| auto inPoint = cubic->renderIn(); | |
| auto translation = cubic->renderTranslation(); | |
| rawPath.cubic(out, inPoint, translation); | |
| prevIsCubic = true; | |
| out = cubic->renderOut(); | |
| } | |
| else | |
| { | |
| auto point = *vertex->as<StraightVertex>(); | |
| Vec2D pos = point.renderTranslation(); | |
| auto radius = point.radius(); | |
| if (radius > 0.0f) | |
| { | |
| auto prev = vertices[i - 1]; | |
| Vec2D toPrev = (prev->is<CubicVertex>() ? prev->as<CubicVertex>()->renderOut() | |
| : prev->renderTranslation()) - | |
| pos; | |
| auto toPrevLength = toPrev.normalizeLength(); | |
| auto next = vertices[(i + 1) % length]; | |
| Vec2D toNext = (next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn() | |
| : next->renderTranslation()) - | |
| pos; | |
| auto toNextLength = toNext.normalizeLength(); | |
| float renderRadius = | |
| std::min(toPrevLength / 2.0f, std::min(toNextLength / 2.0f, radius)); | |
| float idealDistance = | |
| computeIdealControlPointDistance(toPrev, toNext, renderRadius); | |
| Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius); | |
| if (prevIsCubic) | |
| { | |
| rawPath.cubic(out, translation, translation); | |
| } | |
| else | |
| { | |
| rawPath.line(translation); | |
| } | |
| Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance); | |
| Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance); | |
| out = Vec2D::scaleAndAdd(pos, toNext, renderRadius); | |
| rawPath.cubic(outPoint, inPoint, out); | |
| prevIsCubic = false; | |
| } | |
| else if (prevIsCubic) | |
| { | |
| rawPath.cubic(out, pos, pos); | |
| prevIsCubic = false; | |
| out = pos; | |
| } | |
| else | |
| { | |
| out = pos; | |
| rawPath.line(out); | |
| } | |
| } | |
| } | |
| if (isClosed) | |
| { | |
| if (prevIsCubic || startIsCubic) | |
| { | |
| rawPath.cubic(out, startIn, start); | |
| } | |
| else | |
| { | |
| rawPath.line(start); | |
| } | |
| rawPath.close(); | |
| } | |
| } | |
| void Path::markPathDirty(bool sendToLayout) | |
| { | |
| addDirt(ComponentDirt::Path); | |
| if (m_Shape != nullptr) | |
| { | |
| m_Shape->pathChanged(); | |
| } | |
| } | |
| void Path::onDirty(ComponentDirt value) | |
| { | |
| if (hasDirt(value, ComponentDirt::WorldTransform) && m_Shape != nullptr) | |
| { | |
| m_Shape->pathChanged(); | |
| } | |
| if (m_deferredPathDirt) | |
| { | |
| addDirt(ComponentDirt::Path); | |
| } | |
| } | |
| void Path::update(ComponentDirt value) | |
| { | |
| Super::update(value); | |
| if (hasDirt(value, ComponentDirt::Path)) | |
| { | |
| if (canDeferPathUpdate()) | |
| { | |
| m_deferredPathDirt = true; | |
| return; | |
| } | |
| m_deferredPathDirt = false; | |
| // Build path doesn't explicitly rewind because we use it to concatenate | |
| // multiple built paths into a single command path (like the hit | |
| // tester). | |
| m_rawPath.rewind(); | |
| buildPath(m_rawPath); | |
| } | |
| // if (hasDirt(value, ComponentDirt::WorldTransform) && m_Shape != nullptr) | |
| // { | |
| // // Make sure the path composer has an opportunity to rebuild the path | |
| // // (this is why the composer depends on the shape and all its paths, | |
| // // ascertaning it updates after both) | |
| // m_Shape->pathChanged(); | |
| // } | |
| } | |
| bool Path::collapse(bool value) | |
| { | |
| bool changed = Super::collapse(value); | |
| if (changed && m_Shape != nullptr) | |
| { | |
| m_Shape->pathCollapseChanged(); | |
| } | |
| return changed; | |
| } | |
| #ifdef ENABLE_QUERY_FLAT_VERTICES | |
| class DisplayCubicVertex : public CubicVertex | |
| { | |
| public: | |
| DisplayCubicVertex(const Vec2D& in, const Vec2D& out, const Vec2D& translation) | |
| { | |
| m_InPoint = in; | |
| m_OutPoint = out; | |
| m_InValid = true; | |
| m_OutValid = true; | |
| x(translation.x); | |
| y(translation.y); | |
| } | |
| void computeIn() override {} | |
| void computeOut() override {} | |
| }; | |
| FlattenedPath* Path::makeFlat(bool transformToParent) | |
| { | |
| if (m_Vertices.empty()) | |
| { | |
| return nullptr; | |
| } | |
| // Path transform always puts the path into world space. | |
| auto transform = pathTransform(); | |
| if (transformToParent && parent()->is<TransformComponent>()) | |
| { | |
| // Put the transform in parent space. | |
| auto world = parent()->as<TransformComponent>()->worldTransform(); | |
| transform = world.invertOrIdentity() * transform; | |
| } | |
| FlattenedPath* flat = new FlattenedPath(); | |
| auto length = m_Vertices.size(); | |
| PathVertex* previous = isPathClosed() ? m_Vertices[length - 1] : nullptr; | |
| bool deletePrevious = false; | |
| for (size_t i = 0; i < length; i++) | |
| { | |
| auto vertex = m_Vertices[i]; | |
| switch (vertex->coreType()) | |
| { | |
| case StraightVertex::typeKey: | |
| { | |
| auto point = *vertex->as<StraightVertex>(); | |
| if (point.radius() > 0.0f && (isPathClosed() || (i != 0 && i != length - 1))) | |
| { | |
| auto next = m_Vertices[(i + 1) % length]; | |
| Vec2D prevPoint = previous->is<CubicVertex>() | |
| ? previous->as<CubicVertex>()->renderOut() | |
| : previous->renderTranslation(); | |
| Vec2D nextPoint = next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn() | |
| : next->renderTranslation(); | |
| Vec2D pos = point.renderTranslation(); | |
| Vec2D toPrev = prevPoint - pos; | |
| auto toPrevLength = toPrev.normalizeLength(); | |
| Vec2D toNext = nextPoint - pos; | |
| auto toNextLength = toNext.normalizeLength(); | |
| auto renderRadius = std::min(toPrevLength / 2.0f, | |
| std::min(toNextLength / 2.0f, point.radius())); | |
| float idealDistance = | |
| computeIdealControlPointDistance(toPrev, toNext, renderRadius); | |
| Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius); | |
| Vec2D out = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance); | |
| { | |
| auto v1 = new DisplayCubicVertex(translation, out, translation); | |
| flat->addVertex(v1, transform); | |
| delete v1; | |
| } | |
| translation = Vec2D::scaleAndAdd(pos, toNext, renderRadius); | |
| Vec2D in = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance); | |
| auto v2 = new DisplayCubicVertex(in, translation, translation); | |
| flat->addVertex(v2, transform); | |
| if (deletePrevious) | |
| { | |
| delete previous; | |
| } | |
| previous = v2; | |
| deletePrevious = true; | |
| break; | |
| } | |
| RIVE_FALLTHROUGH; | |
| } | |
| default: | |
| if (deletePrevious) | |
| { | |
| delete previous; | |
| } | |
| previous = vertex; | |
| deletePrevious = false; | |
| flat->addVertex(previous, transform); | |
| break; | |
| } | |
| } | |
| if (deletePrevious) | |
| { | |
| delete previous; | |
| } | |
| return flat; | |
| } | |
| void FlattenedPath::addVertex(PathVertex* vertex, const Mat2D& transform) | |
| { | |
| // To make this easy and relatively clean we just transform the vertices. | |
| // Requires the vertex to be passed in as a clone. | |
| if (vertex->is<CubicVertex>()) | |
| { | |
| auto cubic = vertex->as<CubicVertex>(); | |
| // Cubics need to be transformed so we create a Display version which | |
| // has set in/out values. | |
| const auto in = transform * cubic->renderIn(); | |
| const auto out = transform * cubic->renderOut(); | |
| const auto translation = transform * cubic->renderTranslation(); | |
| auto displayCubic = new DisplayCubicVertex(in, out, translation); | |
| m_Vertices.push_back(displayCubic); | |
| } | |
| else | |
| { | |
| auto point = new PathVertex(); | |
| Vec2D translation = transform * vertex->renderTranslation(); | |
| point->x(translation.x); | |
| point->y(translation.y); | |
| m_Vertices.push_back(point); | |
| } | |
| } | |
| FlattenedPath::~FlattenedPath() | |
| { | |
| for (auto vertex : m_Vertices) | |
| { | |
| delete vertex; | |
| } | |
| } | |
| #endif |