blob: 37106d7ef59c3c2529682ac859ef3c49e2075125 [file]
#include <rive/artboard.hpp>
#include <rive/bones/bone.hpp>
#include <rive/bones/root_bone.hpp>
#include <rive/bones/skin.hpp>
#include <rive/bones/tendon.hpp>
#include <rive/bones/weight.hpp>
#include <rive/file.hpp>
#include <rive/shapes/paint/fill.hpp>
#include <rive/shapes/paint/solid_color.hpp>
#include <rive/shapes/points_path.hpp>
#include <rive/shapes/shape.hpp>
#include <rive/shapes/shape_path_flags.hpp>
#include <rive/shapes/straight_vertex.hpp>
#include <utils/no_op_factory.hpp>
#include "rive_file_reader.hpp"
#include <catch.hpp>
#include <cmath>
namespace
{
// A 100x100 quad with a clockwise fill, its top edge bound to one root bone
// and its bottom edge to another.
struct QuadRig
{
rive::NoOpFactory factory;
rive::Artboard artboard{&factory};
rive::RootBone* top = new rive::RootBone();
rive::RootBone* bottom = new rive::RootBone();
rive::Shape* shape = new rive::Shape();
rive::PointsPath* path = new rive::PointsPath();
rive::Skin* skin = new rive::Skin();
std::vector<rive::StraightVertex*> vertices;
QuadRig(bool clockwise)
{
artboard.addObject(&artboard);
auto add = [&](rive::Component* component, rive::Core* parent) {
artboard.addObject(component);
component->parentId(artboard.idOf(parent));
};
add(top, &artboard);
bottom->y(100.0f);
add(bottom, &artboard);
add(shape, &artboard);
auto fill = new rive::Fill();
fill->fillRule((uint32_t)rive::FillRule::clockwise);
add(fill, shape);
add(new rive::SolidColor(), fill);
path->isClosed(true);
if (!clockwise)
{
path->pathFlags((uint32_t)rive::ShapePathFlags::isCounterClockwise);
}
add(path, shape);
float corners[4][2] = {{0, 0}, {100, 0}, {100, 100}, {0, 100}};
for (int i = 0; i < 4; i++)
{
auto corner = corners[clockwise ? i : 3 - i];
auto vertex = new rive::StraightVertex();
vertex->x(corner[0]);
vertex->y(corner[1]);
add(vertex, path);
auto weight = new rive::Weight();
weight->values(255);
// Tendon slots start at 1, the top bone's tendon comes first.
weight->indices(corner[1] == 0 ? 1 : 2);
add(weight, vertex);
vertices.push_back(vertex);
}
add(skin, path);
for (auto bone : {top, bottom})
{
auto tendon = new rive::Tendon();
tendon->boneId(artboard.idOf(bone));
tendon->ty(bone->y());
add(tendon, skin);
}
REQUIRE(artboard.initialize() == rive::StatusCode::Ok);
}
void scale(rive::RootBone* bone, float x, float y)
{
bone->scaleX(x);
bone->scaleY(y);
}
// Area of the path as the clockwise fill receives it, positive when it
// really is clockwise.
float composedArea()
{
artboard.advance(0.0f);
return shape->localClockwisePath()->rawPath()->computeCoarseArea();
}
float deformedArea()
{
auto& rawPath = path->rawPath();
return rawPath.computeCoarseArea(rawPath.bounds().center());
}
};
bool isAncestor(rive::Component* ancestor, rive::Component* of)
{
for (auto c = of->parent(); c != nullptr; c = c->parent())
{
if (c == ancestor)
{
return true;
}
}
return false;
}
// Lowest bone that every tendon of the skin hangs under.
rive::Bone* commonBone(rive::Skin* skin)
{
auto& tendons = skin->tendons();
for (rive::Component* c = tendons[0]->bone(); c != nullptr; c = c->parent())
{
if (!c->is<rive::Bone>())
{
continue;
}
bool coversAll = true;
for (auto tendon : tendons)
{
if (tendon->bone() != c && !isAncestor(c, tendon->bone()))
{
coversAll = false;
break;
}
}
if (coversAll)
{
return c->as<rive::Bone>();
}
}
return nullptr;
}
rive::Fill* firstFill(rive::Shape* shape)
{
for (auto paint : shape->shapePaints())
{
if (paint->is<rive::Fill>())
{
return paint->as<rive::Fill>();
}
}
return nullptr;
}
} // namespace
TEST_CASE("unskinned and resting skinned quads compose clockwise",
"[skinwinding]")
{
for (bool clockwise : {true, false})
{
QuadRig rig(clockwise);
CHECK(rig.composedArea() > 0);
CHECK(rig.skin->windingSign() == 1);
CHECK((rig.deformedArea() > 0) == clockwise);
}
}
TEST_CASE("a collapsed first frame is not cached as the winding",
"[skinwinding]")
{
QuadRig rig(false);
rig.scale(rig.top, 0.0f, 0.0f);
rig.scale(rig.bottom, 0.0f, 0.0f);
rig.artboard.advance(0.0f);
CHECK(rig.skin->windingSign() == 0);
CHECK(rig.deformedArea() == 0.0f);
rig.scale(rig.top, 1.0f, 1.0f);
rig.scale(rig.bottom, 1.0f, 1.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.deformedArea() < 0);
}
TEST_CASE("a collapsed bone does not vote on the mirroring", "[skinwinding]")
{
QuadRig rig(true);
CHECK(rig.composedArea() > 0);
rig.scale(rig.top, -1.0f, 1.0f);
rig.scale(rig.bottom, 0.0f, 0.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.skin->windingSign() == -1);
CHECK(rig.deformedArea() < 0);
rig.scale(rig.bottom, -1.0f, 1.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.skin->windingSign() == -1);
}
TEST_CASE("a mixed first frame leaves nothing wrong cached", "[skinwinding]")
{
QuadRig rig(false);
rig.scale(rig.top, -1.5f, 1.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.skin->windingSign() == 0);
// The half mirrored quad winds the other way from its resting self.
CHECK(rig.deformedArea() > 0);
rig.scale(rig.top, 1.0f, 1.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.skin->windingSign() == 1);
CHECK(rig.deformedArea() < 0);
rig.scale(rig.top, -1.0f, 1.0f);
rig.scale(rig.bottom, -1.0f, 1.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.skin->windingSign() == -1);
CHECK(rig.deformedArea() > 0);
}
TEST_CASE("a rig far from the origin still measures its winding",
"[skinwinding]")
{
QuadRig rig(false);
for (auto bone : {rig.top, rig.bottom})
{
bone->x(1e5f);
bone->y(bone->y() + 1e5f);
}
CHECK(rig.composedArea() > 0);
CHECK(rig.deformedArea() == Approx(-10000.0f));
// Only a cached measurement keeps this right, the authored flag is stale.
rig.scale(rig.top, -1.0f, 1.0f);
rig.scale(rig.bottom, -1.0f, 1.0f);
CHECK(rig.composedArea() > 0);
CHECK(rig.deformedArea() == Approx(10000.0f));
}
TEST_CASE("moving a vertex measures the winding again", "[skinwinding]")
{
QuadRig rig(true);
CHECK(rig.composedArea() > 0);
for (auto vertex : rig.vertices)
{
vertex->x(-vertex->x());
}
CHECK(rig.composedArea() > 0);
CHECK(rig.deformedArea() < 0);
}
// Pins a known limitation so that changing it is deliberate: bones that fold
// the path inside out without mirroring keep the winding measured before.
TEST_CASE("a fold without mirroring keeps the measured winding",
"[skinwinding]")
{
QuadRig rig(true);
CHECK(rig.composedArea() > 0);
rig.top->y(200.0f);
float composed = rig.composedArea();
CHECK(rig.skin->windingSign() == 1);
CHECK(rig.deformedArea() < 0);
CHECK(composed < 0);
}
TEST_CASE("a real rig stays clockwise when its bones mirror", "[skinwinding]")
{
auto file = ReadRiveFile("assets/zombie_skins.riv");
auto source = file->artboard();
// A filled single path shape bound to bones, hung off the artboard so the
// bones can mirror without the shape following.
rive::Shape* sourceShape = nullptr;
rive::Bone* sourceBone = nullptr;
for (auto core : source->objects())
{
if (core == nullptr || !core->is<rive::PointsPath>())
{
continue;
}
auto path = core->as<rive::PointsPath>();
auto shape = path->shape();
if (path->skin() == nullptr || shape->paths().size() != 1 ||
firstFill(shape) == nullptr || path->skin()->tendons().size() < 2)
{
continue;
}
sourceBone = commonBone(path->skin());
if (sourceBone != nullptr)
{
sourceShape = shape;
break;
}
}
REQUIRE(sourceShape != nullptr);
sourceShape->parentId(0);
auto artboard = source->instance();
auto shape =
artboard->objects()[source->idOf(sourceShape)]->as<rive::Shape>();
auto bone = artboard->objects()[source->idOf(sourceBone)]->as<rive::Bone>();
REQUIRE(shape->parent() == artboard.get());
REQUIRE(!isAncestor(bone, shape));
auto path = shape->paths()[0]->as<rive::PointsPath>();
auto skin = path->skin();
firstFill(shape)->fillRule((uint32_t)rive::FillRule::clockwise);
shape->pathChanged();
artboard->advance(0.0f);
float restArea = path->rawPath().computeCoarseArea();
CHECK(shape->localClockwisePath()->rawPath()->computeCoarseArea() > 0);
CHECK(skin->windingSign() == 1);
bone->scaleX(-1.0f);
artboard->advance(0.0f);
CHECK(skin->windingSign() == -1);
CHECK(path->rawPath().computeCoarseArea() * restArea < 0);
CHECK(shape->localClockwisePath()->rawPath()->computeCoarseArea() > 0);
bone->scaleX(1.0f);
// Mirror the first bone no other tendon hangs under, folding the path.
rive::Bone* leaf = nullptr;
for (auto tendon : skin->tendons())
{
bool hasChildTendon = false;
for (auto other : skin->tendons())
{
hasChildTendon |= isAncestor(tendon->bone(), other->bone());
}
if (!hasChildTendon && tendon->bone() != bone)
{
leaf = tendon->bone();
break;
}
}
REQUIRE(leaf != nullptr);
leaf->scaleX(-1.0f);
artboard->advance(0.0f);
CHECK(skin->windingSign() == 0);
// The fold has to have moved the path for this to test anything.
float foldedArea = path->rawPath().computeCoarseArea();
CHECK(std::abs(foldedArea - restArea) > 0.01f * std::abs(restArea));
CHECK(shape->localClockwisePath()->rawPath()->computeCoarseArea() *
foldedArea >=
0);
}