blob: 4204059a60b651f2162d4195350177e0ef9b4f87 [file]
/*
* Copyright 2026 Rive
*
* The one definition of how far past its geometry a paint reaches. Two
* independent consumers depend on this table agreeing:
*
* - RiveRenderPath::calculateBoundsOutset, which culls draws;
* - offscreen raster sizing, which decides how big a layer-mask surface is.
*
* and a third, the editor's Dart mirror, is pinned against the same numbers.
* If a case here changes, the Dart table has to change with it or the editor
* preview will crop where playback does not.
*/
#include <rive/shapes/paint/paint_outset.hpp>
#include <catch.hpp>
using namespace rive;
TEST_CASE("no stroke and no feather reaches nowhere", "[paint-outset]")
{
CHECK(paintBoundsOutset({}, 0.0f) == 0.0f);
}
TEST_CASE("a stroke reaches half its thickness", "[paint-outset]")
{
StrokeParams s;
s.thickness = 10.0f;
s.join = StrokeJoin::round;
s.cap = StrokeCap::butt;
CHECK(paintBoundsOutset(s, 0.0f) == Approx(5.0f));
// Zero thickness is a real authored state and must not be special-cased
// into something non-zero.
s.thickness = 0.0f;
CHECK(paintBoundsOutset(s, 0.0f) == 0.0f);
}
TEST_CASE("a miter join overshoots the corner by the miter limit",
"[paint-outset]")
{
StrokeParams s;
s.thickness = 10.0f;
s.join = StrokeJoin::miter;
s.cap = StrokeCap::butt;
CHECK(paintBoundsOutset(s, 0.0f) == Approx(5.0f * kMiterLimit));
CHECK(paintBoundsOutset(s, 0.0f) == Approx(20.0f));
}
TEST_CASE("a square cap overshoots the end by its diagonal", "[paint-outset]")
{
StrokeParams s;
s.thickness = 10.0f;
s.join = StrokeJoin::round;
s.cap = StrokeCap::square;
CHECK(paintBoundsOutset(s, 0.0f) == Approx(5.0f * math::SQRT2));
// Bevel joins do not overshoot, so the cap still decides.
s.join = StrokeJoin::bevel;
CHECK(paintBoundsOutset(s, 0.0f) == Approx(5.0f * math::SQRT2));
}
TEST_CASE("a miter join outranks a square cap", "[paint-outset]")
{
// Not additive, and the miter branch wins: it is the larger of the two and
// the two overshoots cannot both happen at the same vertex.
StrokeParams s;
s.thickness = 10.0f;
s.join = StrokeJoin::miter;
s.cap = StrokeCap::square;
CHECK(paintBoundsOutset(s, 0.0f) == Approx(5.0f * kMiterLimit));
}
TEST_CASE("a feather reaches 1.5x its authored strength", "[paint-outset]")
{
// Design tools quote a blur as the width of two standard deviations, so an
// authored 10 bleeds 15 in every direction.
CHECK(featherRadiusFromFeather(10.0f) == Approx(15.0f));
CHECK(paintBoundsOutset({}, 10.0f) == Approx(15.0f));
CHECK(paintBoundsOutset({}, 0.0f) == 0.0f);
}
TEST_CASE("a feather adds to a stroke rather than replacing it",
"[paint-outset]")
{
// These two DO compound: the stroke widens the shape and the blur spreads
// the widened edge.
StrokeParams s;
s.thickness = 10.0f;
s.join = StrokeJoin::round;
s.cap = StrokeCap::butt;
CHECK(paintBoundsOutset(s, 10.0f) == Approx(5.0f + 15.0f));
s.join = StrokeJoin::miter;
CHECK(paintBoundsOutset(s, 10.0f) == Approx(20.0f + 15.0f));
}
TEST_CASE("the outset never shrinks the box", "[paint-outset]")
{
// Whatever combination is authored, the result is an over-estimate or exact
// -- never negative, which would crop.
for (float thickness : {0.0f, 0.5f, 1.0f, 40.0f})
{
for (float feather : {0.0f, 0.25f, 30.0f})
{
for (auto join :
{StrokeJoin::miter, StrokeJoin::round, StrokeJoin::bevel})
{
for (auto cap :
{StrokeCap::butt, StrokeCap::round, StrokeCap::square})
{
StrokeParams s;
s.thickness = thickness;
s.join = join;
s.cap = cap;
const float outset = paintBoundsOutset(s, feather);
CHECK(outset >= 0.0f);
// Must cover the naive half-thickness plus the blur.
CHECK(outset >= Approx(thickness * 0.5f +
featherRadiusFromFeather(feather))
.margin(1e-4f));
}
}
}
}
}