blob: 6bee1475572b7e15ebb55f9a99cca81985bab97a [file]
#include "rive/semantic/semantic_provider.hpp"
#include "rive/artboard.hpp"
#include "rive/component.hpp"
#include "rive/container_component.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/node.hpp"
#include "rive/semantic/semantic_data.hpp"
#include "semantic_inference_registry.hpp"
using namespace rive;
static bool tryNodeWorldBounds(Node* node, AABB& outBounds)
{
if (node == nullptr)
{
return false;
}
const auto localBounds = node->localBounds();
if (localBounds.isEmptyOrNaN())
{
return false;
}
const auto worldTransform = node->worldTransform();
const auto worldBounds = worldTransform.mapBoundingBox(localBounds);
if (worldBounds.isEmptyOrNaN())
{
return false;
}
outBounds = worldBounds;
return true;
}
AABB SemanticProvider::rootTransformAABB(Artboard* ab, const AABB& bounds)
{
AABB out = AABB::forExpansion();
AABB::expandTo(out, ab->rootTransform({bounds.minX, bounds.minY}));
AABB::expandTo(out, ab->rootTransform({bounds.maxX, bounds.minY}));
AABB::expandTo(out, ab->rootTransform({bounds.maxX, bounds.maxY}));
AABB::expandTo(out, ab->rootTransform({bounds.minX, bounds.maxY}));
return out;
}
bool SemanticProvider::canInferSemantics(Component* component)
{
return supportsInferredSemantics(component);
}
ResolvedSemanticData SemanticProvider::resolveSemanticData(Component* component)
{
ResolvedSemanticData out;
if (component == nullptr || !component->is<Node>())
{
return out;
}
if (auto* sd = component->as<Node>()->firstChild<SemanticData>())
{
out.hasSemantics = true;
out.role = sd->role();
out.label = sd->label();
return out;
}
// Inference fallback (Text -> label)
resolveInferredSemantics(component, out);
return out;
}
// Computes world-space bounds for a component. Three strategies:
// 1. Drawable nodes: localBounds() → worldTransform → rootTransform
// 2. Container nodes: merge bounds of all direct Node children
// 3. Fallback: world transform translation (degenerate point bounds)
AABB SemanticProvider::semanticBounds(Component* component)
{
if (component == nullptr || !component->is<Node>())
{
return AABB();
}
auto* node = component->as<Node>();
AABB worldBounds;
if (tryNodeWorldBounds(node, worldBounds))
{
auto* ab = node->artboard();
if (ab != nullptr)
{
return rootTransformAABB(ab, worldBounds);
}
return worldBounds;
}
// If this semantic node is a non-drawable container/group, derive bounds
// from visual descendant nodes. ContainerComponent::forEachChild walks
// the whole subtree (not just direct children): the predicate runs for
// each descendant, and returning `true` tells forEachChild to keep
// descending into that descendant's children. We always return true so
// every Node descendant contributes bounds; non-Node descendants and
// Nodes without valid bounds are skipped but their subtrees are still
// walked.
if (component->is<ContainerComponent>())
{
AABB merged = AABB::forExpansion();
bool hasDescendantBounds = false;
component->as<ContainerComponent>()->forEachChild(
[&](Component* child) -> bool {
if (child == nullptr || !child->is<Node>())
{
return true;
}
AABB childBounds;
if (!tryNodeWorldBounds(child->as<Node>(), childBounds))
{
return true;
}
merged.expand(childBounds);
hasDescendantBounds = true;
return true;
});
if (hasDescendantBounds)
{
auto* ab = node->artboard();
if (ab != nullptr)
{
return rootTransformAABB(ab, merged);
}
return merged;
}
}
// Fallback for nodes/components without local bounds.
const auto worldTransform = node->worldTransform();
const float x = worldTransform[4];
const float y = worldTransform[5];
auto* ab = node->artboard();
if (ab != nullptr)
{
Vec2D pos = ab->rootTransform(Vec2D(x, y));
return AABB(pos.x, pos.y, pos.x, pos.y);
}
return AABB(x, y, x, y);
}