renderer: revise the paint bounding box api

C++ API Modificaiton:
 - Result Paint::bounds(float* x, float* y, float* w, float* h, bool transform = false) const
  -> Result Paint::bounds(float* x, float* y, float* w, float* h) const

C++ API Addition:
 - Result Paint::bounds(Point* pt4) const

C API Modification:
- Tvg_Result tvg_paint_get_bounds(const Tvg_Paint* paint, float* x, float* y, float* w, float* h, bool transformed);
 -> Tvg_Result tvg_paint_get_aabb(const Tvg_Paint* paint, float* x, float* y, float* w, float* h);

C API Addition:
- Tvg_Result tvg_paint_get_obb(const Tvg_Paint* paint, Tvg_Point* pt4);

issue: https://github.com/thorvg/thorvg/issues/3290
This commit is contained in:
Hermet Park 2025-03-04 19:42:58 +09:00 committed by Hermet Park
parent 259dbc9840
commit 67098793f0
14 changed files with 212 additions and 149 deletions

View file

@ -97,13 +97,30 @@ struct UserExample : tvgexam::Example
bool clicked(tvg::Canvas* canvas, int32_t x, int32_t y) override
{
auto intersect = [&](float x, float y, tvg::Point* obb) -> bool {
//compute edge vectors
tvg::Point e1 = {obb[1].x - obb[0].x, obb[1].y - obb[0].y}; // Edge from obb[0] to obb[1]
tvg::Point e2 = {obb[3].x - obb[0].x, obb[3].y - obb[0].y}; // Edge from obb[0] to obb[3]
//compute vectors from obb[0] to the test point
tvg::Point o = {x - obb[0].x, y - obb[0].y};
/* compute dot products to express `o` in terms of edge1 and edge2
and then compute barycentric coordinates (u, v) within the box space */
auto u = (o.x * e1.x + o.y * e1.y) / (e1.x * e1.x + e1.y * e1.y);
auto v = (o.x * e2.x + o.y * e2.y) / (e2.x * e2.x + e2.y * e2.y);
// Check if point is inside the OBB
return (u >= 0.0f && u <= 1.0f && v >= 0.0f && v <= 1.0f);
};
int i = 0;
for (auto& state : states) {
if (auto paint = lottie->picture()->paint(tvg::Accessor::id(state.name.c_str()))) {
float px, py, pw, ph;
paint->bounds(&px, &py, &pw, &ph, true);
tvg::Point obb[4];
paint->bounds(obb);
//hit a emoji layer!
if (x >= px && x <= px + pw && y >= py && y <= py + ph) {
if (intersect(x, y, obb)) {
tweening(i);
return true;
}

View file

@ -412,20 +412,42 @@ public:
Result blend(BlendMethod method) noexcept;
/**
* @brief Gets the axis-aligned bounding box of the paint object.
* @brief Retrieves the object-oriented bounding box (OBB) of the paint object in canvas space.
*
* This function returns the bounding box of the paint, as an oriented bounding box (OBB) after transformations are applied.
*
* @param[out] x The x-coordinate of the upper-left corner of the object.
* @param[out] y The y-coordinate of the upper-left corner of the object.
* @param[out] w The width of the object.
* @param[out] h The height of the object.
* @param[in] transformed If @c true, the paint's transformations are taken into account in the scene it belongs to. Otherwise they aren't.
* @param[out] pt4 An array of four points representing the bounding box. The array size must be 4.
*
* @note This is useful when you need to figure out the bounding box of the paint in the canvas space.
* @note The bounding box doesn't indicate the actual drawing region. It's the smallest rectangle that encloses the object.
* @note If @p transformed is @c true, the paint needs to be pushed into a canvas and updated before this api is called.
* @retval Result::InvalidArguments @p pt4 is @c nullptr.
* @retval Result::InsufficientCondition If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The paint must be pushed into a canvas and updated before calling this function.
*
* @see Paint::bounds(float* x, float* y, folat* w, float* h)
* @see Canvas::update()
*
* @since 1.0
*/
Result bounds(Point* pt4) const noexcept;
/**
* @brief Retrieves the axis-aligned bounding box (AABB) of the paint object in local space.
*
* This function returns the bounding box of the paint object relative to its local coordinate system, without applying any transformations.
*
* @param[out] x The x-coordinate of the upper-left corner of the bounding box.
* @param[out] y The y-coordinate of the upper-left corner of the bounding box.
* @param[out] w The width of the bounding box.
* @param[out] h The height of the bounding box.
*
* @retval Result::InsufficientCondition If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The bounding box is calculated in the object's local space, meaning transformations such as scaling, rotation, or translation are not applied.
*
* @see Paint::bounds(Point* pt4)
* @see Canvas::update()
*/
Result bounds(float* x, float* y, float* w, float* h, bool transformed = false) const noexcept;
Result bounds(float* x, float* y, float* w, float* h) const noexcept;
/**
* @brief Duplicates the object.

View file

@ -912,25 +912,49 @@ TVG_API Tvg_Result tvg_paint_get_opacity(const Tvg_Paint* paint, uint8_t* opacit
TVG_API Tvg_Paint* tvg_paint_duplicate(Tvg_Paint* paint);
/*!
* @brief Gets the axis-aligned bounding box of the Tvg_Paint object.
*
* @param[in] paint The Tvg_Paint object of which to get the bounds.
* @param[out] x The x-coordinate of the upper-left corner of the object.
* @param[out] y The y-coordinate of the upper-left corner of the object.
* @param[out] w The width of the object.
* @param[out] h The height of the object.
* @param[in] transformed If @c true, the paint's transformations are taken into account in the scene it belongs to. Otherwise they aren't.
*
* @return Tvg_Result enumeration.
* @retval TVG_RESULT_INVALID_ARGUMENT An invalid Tvg_Paint pointer.
*
* @note This is useful when you need to figure out the bounding box of the paint in the canvas space.
* @note The bounding box doesn't indicate the actual drawing region. It's the smallest rectangle that encloses the object.
* @note If @p transformed is @c true, the paint needs to be pushed into a canvas and updated before this api is called.
* @see tvg_canvas_update_paint()
*/
TVG_API Tvg_Result tvg_paint_get_bounds(const Tvg_Paint* paint, float* x, float* y, float* w, float* h, bool transformed);
/**
* @brief Retrieves the axis-aligned bounding box (AABB) of the paint object in local space.
*
* This function returns the bounding box of the paint object relative to its local coordinate system, without applying any transformations.
*
* @param[in] paint The Tvg_Paint object of which to get the bounds.
* @param[out] x The x-coordinate of the upper-left corner of the bounding box.
* @param[out] y The y-coordinate of the upper-left corner of the bounding box.
* @param[out] w The width of the bounding box.
* @param[out] h The height of the bounding box.
*
* @return Tvg_Result enumeration.
* @retval TVG_RESULT_INVALID_ARGUMENT An invalid @p paint.
* @retval TVG_RESULT_INSUFFICIENT_CONDITION If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The bounding box is calculated in the object's local space, meaning transformations such as scaling, rotation, or translation are not applied.
*
* @see tvg_paint_get_obb()
* @see tvg_canvas_update_paint()
*/
TVG_API Tvg_Result tvg_paint_get_aabb(const Tvg_Paint* paint, float* x, float* y, float* w, float* h);
/**
* @brief Retrieves the object-oriented bounding box (OBB) of the paint object in canvas space.
*
* This function returns the bounding box of the paint, as an oriented bounding box (OBB) after transformations are applied.
*
* @param[in] paint The Tvg_Paint object of which to get the bounds.
* @param[out] pt4 An array of four points representing the bounding box. The array size must be 4.
*
* @return Tvg_Result enumeration.
* @retval TVG_RESULT_INVALID_ARGUMENT @p paint or @p pt4 is invalid.
* @retval TVG_RESULT_INSUFFICIENT_CONDITION If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The paint must be pushed into a canvas and updated before calling this function.
*
* @see tvg_paint_get_aabb()
* @see tvg_canvas_update_paint()
*
* @since 1.0
*/
TVG_API Tvg_Result tvg_paint_get_obb(const Tvg_Paint* paint, Tvg_Point* pt4);
/*!
@ -941,7 +965,7 @@ TVG_API Tvg_Result tvg_paint_get_bounds(const Tvg_Paint* paint, float* x, float*
* @param[in] method The method used to mask the source object with the target.
*
* @return Tvg_Result enumeration.
* @retval TVG_RESULT_INVALID_ARGUMENT An invalid @p paint or @p target object or the @p method equal to TVG_MASK_METHOD_NONE.
*/
TVG_API Tvg_Result tvg_paint_set_mask_method(Tvg_Paint* paint, Tvg_Paint* target, Tvg_Mask_Method method);

View file

@ -256,12 +256,17 @@ TVG_API Tvg_Result tvg_paint_get_opacity(const Tvg_Paint* paint, uint8_t* opacit
}
TVG_API Tvg_Result tvg_paint_get_bounds(const Tvg_Paint* paint, float* x, float* y, float* w, float* h, bool transformed)
TVG_API Tvg_Result tvg_paint_get_aabb(const Tvg_Paint* paint, float* x, float* y, float* w, float* h)
{
if (!paint) return TVG_RESULT_INVALID_ARGUMENT;
return (Tvg_Result) reinterpret_cast<const Paint*>(paint)->bounds(x, y, w, h, transformed);
return (Tvg_Result) reinterpret_cast<const Paint*>(paint)->bounds(x, y, w, h);
}
TVG_API Tvg_Result tvg_paint_get_obb(const Tvg_Paint* paint, Tvg_Point* pt4)
{
if (!paint) return TVG_RESULT_INVALID_ARGUMENT;
return (Tvg_Result) reinterpret_cast<const Paint*>(paint)->bounds((Point*)pt4);
}
TVG_API Tvg_Result tvg_paint_set_mask_method(Tvg_Paint* paint, Tvg_Paint* target, Tvg_Mask_Method method)
{

View file

@ -927,7 +927,7 @@ static void _fontText(LottieText* text, Scene* scene, float frameNo, LottieExpre
txt->fill(doc.color.rgb[0], doc.color.rgb[1], doc.color.rgb[2]);
float width;
txt->bounds(nullptr, nullptr, &width, nullptr, false);
txt->bounds(nullptr, nullptr, &width, nullptr);
auto cursorX = width * doc.justify;
txt->translate(cursorX, -doc.size * 100.0f);

View file

@ -47,26 +47,10 @@ static inline bool _isGroupType(SvgNodeType type)
//According to: https://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBoxUnits (the last paragraph)
//a stroke width should be ignored for bounding box calculations
static Box _boundingBox(const Shape* shape)
static Box _boundingBox(Paint* shape)
{
float x, y, w, h;
shape->bounds(&x, &y, &w, &h, false);
if (auto strokeW = shape->strokeWidth()) {
x += 0.5f * strokeW;
y += 0.5f * strokeW;
w -= strokeW;
h -= strokeW;
}
return {x, y, w, h};
}
static Box _boundingBox(const Text* text)
{
float x, y, w, h;
text->bounds(&x, &y, &w, &h, false);
PAINT(shape)->bounds(&x, &y, &w, &h, false);
return {x, y, w, h};
}
@ -260,7 +244,7 @@ static Matrix _compositionTransform(Paint* paint, const SvgNode* node, const Svg
}
if (!compNode->node.clip.userSpace) {
float x, y, w, h;
PAINT(paint)->bounds(&x, &y, &w, &h, false, false);
PAINT(paint)->bounds(&x, &y, &w, &h, false);
m *= {w, 0, x, 0, h, y, 0, 0, 1};
}
return m;
@ -346,7 +330,7 @@ static Paint* _applyFilter(SvgLoaderData& loaderData, Paint* paint, const SvgNod
auto scene = Scene::gen();
Box bbox{};
paint->bounds(&bbox.x, &bbox.y, &bbox.w, &bbox.h, false);
paint->bounds(&bbox.x, &bbox.y, &bbox.w, &bbox.h);
Box clipBox = filter.filterUserSpace ? filter.box : Box{bbox.x + filter.box.x * bbox.w, bbox.y + filter.box.y * bbox.h, filter.box.w * bbox.w, filter.box.h * bbox.h};
auto primitiveUserSpace = filter.primitiveUserSpace;
auto sx = paint->transform().e11;
@ -943,13 +927,13 @@ static Scene* _sceneBuildHelper(SvgLoaderData& loaderData, const SvgNode* node,
}
static void _updateInvalidViewSize(const Scene* scene, Box& vBox, float& w, float& h, SvgViewFlag viewFlag)
static void _updateInvalidViewSize(Scene* scene, Box& vBox, float& w, float& h, SvgViewFlag viewFlag)
{
bool validWidth = (viewFlag & SvgViewFlag::Width);
bool validHeight = (viewFlag & SvgViewFlag::Height);
float x, y;
scene->bounds(&x, &y, &vBox.w, &vBox.h, false);
scene->bounds(&x, &y, &vBox.w, &vBox.h);
if (!validWidth && !validHeight) {
vBox.x = x;
vBox.y = y;

View file

@ -277,48 +277,41 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const Matrix& pm, Array<R
}
bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking, bool origin)
bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool stroking)
{
Point pts[4];
if (!bounds(pts, false, stroking, false)) return false;
Point min = {FLT_MAX, FLT_MAX};
Point max = {-FLT_MAX, -FLT_MAX};
for (int i = 0; i < 4; ++i) {
if (pts[i].x < min.x) min.x = pts[i].x;
if (pts[i].x > max.x) max.x = pts[i].x;
if (pts[i].y < min.y) min.y = pts[i].y;
if (pts[i].y > max.y) max.y = pts[i].y;
}
if (x) *x = min.x;
if (y) *y = min.y;
if (w) *w = max.x - min.x;
if (h) *h = max.y - min.y;
return true;
}
bool Paint::Impl::bounds(Point* pt4, bool transformed, bool stroking, bool origin)
{
bool ret;
PAINT_METHOD(ret, bounds(pt4, stroking));
if (!ret || !transformed) return ret;
const auto& m = this->transform(origin);
//Case: No transformed, quick return!
if (!transformed || identity(&m)) {
PAINT_METHOD(ret, bounds(x, y, w, h, stroking));
return ret;
}
//Case: Transformed
auto tx = 0.0f;
auto ty = 0.0f;
auto tw = 0.0f;
auto th = 0.0f;
PAINT_METHOD(ret, bounds(&tx, &ty, &tw, &th, stroking));
//Get vertices
Point pt[4] = {{tx, ty}, {tx + tw, ty}, {tx + tw, ty + th}, {tx, ty + th}};
//New bounding box
auto x1 = FLT_MAX;
auto y1 = FLT_MAX;
auto x2 = -FLT_MAX;
auto y2 = -FLT_MAX;
//Compute the AABB after transformation
for (int i = 0; i < 4; i++) {
pt[i] *= m;
if (pt[i].x < x1) x1 = pt[i].x;
if (pt[i].x > x2) x2 = pt[i].x;
if (pt[i].y < y1) y1 = pt[i].y;
if (pt[i].y > y2) y2 = pt[i].y;
}
if (x) *x = x1;
if (y) *y = y1;
if (w) *w = x2 - x1;
if (h) *h = y2 - y1;
pt4[0] *= m;
pt4[1] *= m;
pt4[2] *= m;
pt4[3] *= m;
return ret;
}
@ -373,9 +366,17 @@ Matrix& Paint::transform() noexcept
}
Result Paint::bounds(float* x, float* y, float* w, float* h, bool transformed) const noexcept
Result Paint::bounds(float* x, float* y, float* w, float* h) const noexcept
{
if (pImpl->bounds(x, y, w, h, transformed, true, transformed)) return Result::Success;
if (pImpl->bounds(x, y, w, h, true)) return Result::Success;
return Result::InsufficientCondition;
}
Result Paint::bounds(Point* pt4) const noexcept
{
if (!pt4) return Result::InvalidArguments;
if (pImpl->bounds(pt4, true, true, true)) return Result::Success;
return Result::InsufficientCondition;
}

View file

@ -256,7 +256,8 @@ namespace tvg
RenderRegion bounds(RenderMethod* renderer) const;
Iterator* iterator();
bool bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking, bool origin = false);
bool bounds(float* x, float* y, float* w, float* h, bool stroking);
bool bounds(Point* pt4, bool transformed, bool stroking, bool origin = false);
RenderData update(RenderMethod* renderer, const Matrix& pm, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper = false);
bool render(RenderMethod* renderer);
Paint* duplicate(Paint* ret = nullptr);

View file

@ -114,13 +114,12 @@ struct Picture::Impl : Paint::Impl
return Result::Success;
}
bool bounds(float* x, float* y, float* w, float* h, bool stroking)
bool bounds(Point* pt4, bool stroking)
{
if (x) *x = 0;
if (y) *y = 0;
if (w) *w = this->w;
if (h) *h = this->h;
pt4[0] = {0.0f, 0.0f};
pt4[1] = {w, 0.0f};
pt4[2] = {w, h};
pt4[3] = {0.0f, h};
return true;
}

View file

@ -194,34 +194,28 @@ struct Scene::Impl : Paint::Impl
return ret;
}
bool bounds(float* px, float* py, float* pw, float* ph, bool stroking)
bool bounds(Point* pt4, bool stroking)
{
if (paints.empty()) return false;
auto x1 = FLT_MAX;
auto y1 = FLT_MAX;
auto x2 = -FLT_MAX;
auto y2 = -FLT_MAX;
Point min = {FLT_MAX, FLT_MAX};
Point max = {-FLT_MAX, -FLT_MAX};
for (auto paint : paints) {
auto x = FLT_MAX;
auto y = FLT_MAX;
auto w = 0.0f;
auto h = 0.0f;
if (!PAINT(paint)->bounds(&x, &y, &w, &h, true, stroking)) continue;
Point tmp[4];
if (!PAINT(paint)->bounds(tmp, true, stroking)) continue;
//Merge regions
if (x < x1) x1 = x;
if (x2 < x + w) x2 = (x + w);
if (y < y1) y1 = y;
if (y2 < y + h) y2 = (y + h);
for (int i = 0; i < 4; ++i) {
if (tmp[i].x < min.x) min.x = tmp[i].x;
if (tmp[i].x > max.x) max.x = tmp[i].x;
if (tmp[i].y < min.y) min.y = tmp[i].y;
if (tmp[i].y > max.y) max.y = tmp[i].y;
}
}
if (px) *px = x1;
if (py) *py = y1;
if (pw) *pw = (x2 - x1);
if (ph) *ph = (y2 - y1);
pt4[0] = min;
pt4[1] = {max.x, min.y};
pt4[2] = max;
pt4[3] = {min.x, max.y};
return true;
}

View file

@ -116,17 +116,24 @@ struct Shape::Impl : Paint::Impl
return renderer->region(rd);
}
bool bounds(float* x, float* y, float* w, float* h, bool stroking)
bool bounds(Point* pt4, bool stroking)
{
if (!rs.path.bounds(x, y, w, h)) return false;
float x, y, w, h;
if (!rs.path.bounds(&x, &y, &w, &h)) return false;
//Stroke feathering
if (stroking && rs.stroke) {
if (x) *x -= rs.stroke->width * 0.5f;
if (y) *y -= rs.stroke->width * 0.5f;
if (w) *w += rs.stroke->width;
if (h) *h += rs.stroke->width;
x -= rs.stroke->width * 0.5f;
y -= rs.stroke->width * 0.5f;
w += rs.stroke->width;
h += rs.stroke->width;
}
pt4[0] = {x, y};
pt4[1] = {x + w, y};
pt4[2] = {x + w, y + h};
pt4[3] = {x, y + h};
return true;
}

View file

@ -134,10 +134,10 @@ struct Text::Impl : Paint::Impl
return PAINT(shape)->update(renderer, transform, clips, opacity, pFlag, false);
}
bool bounds(float* x, float* y, float* w, float* h, TVG_UNUSED bool stroking)
bool bounds(Point* pt4, TVG_UNUSED bool stroking)
{
if (!load()) return false;
PAINT(shape)->bounds(x, y, w, h, true, true, false);
PAINT(shape)->bounds(pt4, true, true, false);
return true;
}

View file

@ -124,7 +124,7 @@ bool GifSaver::save(Animation* animation, Paint* bg, const char* filename, TVG_U
auto picture = animation->picture();
float x, y;
x = y = 0;
picture->bounds(&x, &y, &vsize[0], &vsize[1], false);
picture->bounds(&x, &y, &vsize[0], &vsize[1]);
//cut off the negative space
if (x < 0) vsize[0] += x;

View file

@ -127,23 +127,28 @@ TEST_CASE("Bounding Box", "[tvgPaint]")
//Negative
float x = 0, y = 0, w = 0, h = 0;
REQUIRE(shape->bounds(&x, &y, &w, &h, false) == Result::InsufficientCondition);
REQUIRE(shape->bounds(&x, &y, &w, &h) == Result::InsufficientCondition);
//Case 1
REQUIRE(shape->appendRect(0.0f, 10.0f, 20.0f, 100.0f, 50.0f, 50.0f) == Result::Success);
REQUIRE(shape->translate(100.0f, 111.0f) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h, false) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h) == Result::Success);
REQUIRE(x == 0.0f);
REQUIRE(y == 10.0f);
REQUIRE(w == 20.0f);
REQUIRE(h == 100.0f);
REQUIRE(canvas->update(shape) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h, true) == Result::Success);
REQUIRE(x == 100.0f);
REQUIRE(y == 121.0f);
REQUIRE(w == 20.0f);
REQUIRE(h == 100.0f);
Point pts[4];
REQUIRE(shape->bounds(pts) == Result::Success);
REQUIRE(pts[0].x == 100.0f);
REQUIRE(pts[3].x == 100.0f);
REQUIRE(pts[0].y == 121.0f);
REQUIRE(pts[1].y == 121.0f);
REQUIRE(pts[1].x == 120.0f);
REQUIRE(pts[2].x == 120.0f);
REQUIRE(pts[2].y == 221.0f);
REQUIRE(pts[3].y == 221.0f);
//Case 2
REQUIRE(shape->reset() == Result::Success);
@ -151,18 +156,22 @@ TEST_CASE("Bounding Box", "[tvgPaint]")
REQUIRE(shape->lineTo(20.0f, 210.0f) == Result::Success);
auto identity = Matrix{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
REQUIRE(shape->transform(identity) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h, false) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h) == Result::Success);
REQUIRE(x == 0.0f);
REQUIRE(y == 10.0f);
REQUIRE(w == 20.0f);
REQUIRE(h == 200.0f);
REQUIRE(canvas->update(shape) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h, true) == Result::Success);
REQUIRE(x == 0.0f);
REQUIRE(y == 10.0f);
REQUIRE(w == 20.0f);
REQUIRE(h == 200.0f);
REQUIRE(shape->bounds(pts) == Result::Success);
REQUIRE(pts[0].x == 0.0f);
REQUIRE(pts[3].x == 0.0f);
REQUIRE(pts[0].y == 10.0f);
REQUIRE(pts[1].y == 10.0f);
REQUIRE(pts[1].x == 20.0f);
REQUIRE(pts[2].x == 20.0f);
REQUIRE(pts[2].y == 210.0f);
REQUIRE(pts[3].y == 210.0f);
Initializer::term();
}