| #include "rive/layout/layout_participant.hpp" | |
| #include "rive/layout_component.hpp" | |
| #include "rive/layout/layout_component_style.hpp" | |
| #include "rive/layout/layout_node_style.hpp" | |
| #include "rive/layout/grid_item_placement.hpp" | |
| #include "rive/layout/grid_track.hpp" | |
| #include "rive/layout/layout_data.hpp" | |
| #include "rive/layout/layout_style_applier.hpp" | |
| #include "rive/constraints/layout_constraint.hpp" | |
| #include "rive/transform_component.hpp" | |
| #include "rive/intrinsically_sizeable.hpp" | |
| #include "rive/component.hpp" | |
| #include "rive/solo.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/animation/keyframe_interpolator.hpp" | |
| #include <algorithm> | |
| #include <cmath> | |
| using namespace rive; | |
| #ifdef WITH_RIVE_LAYOUT | |
| void LayoutParticipant::addLayoutStyleApplier(LayoutStyleApplier* applier) | |
| { | |
| if (m_layoutData != nullptr) | |
| { | |
| m_layoutData->addApplier(applier); | |
| } | |
| } | |
| #endif | |
| namespace rive | |
| { | |
| // Lazily-allocated animation state — only exists while a participant is under | |
| // an animated layout, so a non-animating participant pays only the pointer. | |
| struct ParticipantAnimation | |
| { | |
| Layout animatedLayout; | |
| LayoutAnimationData a; | |
| LayoutAnimationData b; | |
| bool isSmoothing = false; | |
| LayoutStyleInterpolation interpolation = LayoutStyleInterpolation::hold; | |
| KeyFrameInterpolator* interpolator = nullptr; | |
| float interpolationTime = 0.0f; | |
| }; | |
| } // namespace rive | |
| #ifdef WITH_RIVE_LAYOUT | |
| static YGSize participantMeasureFunc(YGNode* node, | |
| float width, | |
| YGMeasureMode widthMode, | |
| float height, | |
| YGMeasureMode heightMode) | |
| { | |
| auto* component = static_cast<Component*>(node->getContext()); | |
| auto* sizeable = IntrinsicallySizeable::from(component); | |
| Vec2D size = sizeable != nullptr | |
| ? sizeable->measureLayout(width, | |
| (LayoutMeasureMode)widthMode, | |
| height, | |
| (LayoutMeasureMode)heightMode) | |
| : Vec2D(); | |
| return YGSize{size.x, size.y}; | |
| } | |
| #endif | |
| LayoutParticipant::~LayoutParticipant() | |
| { | |
| delete m_animation; | |
| #ifdef WITH_RIVE_LAYOUT | |
| releaseLayoutData(); | |
| #endif | |
| } | |
| TransformComponent* LayoutParticipant::transformComponent() | |
| { | |
| auto* p = parent(); | |
| return (p != nullptr && p->is<TransformComponent>()) | |
| ? p->as<TransformComponent>() | |
| : nullptr; | |
| } | |
| void LayoutParticipant::applyLayoutConstraints() | |
| { | |
| for (auto parentConstraint : layoutConstraints()) | |
| { | |
| parentConstraint->constrainChild(this); | |
| } | |
| } | |
| LayoutComponent* LayoutParticipant::owningLayout() | |
| { | |
| for (Component* c = parent(); c != nullptr; c = c->parent()) | |
| { | |
| if (c->is<LayoutComponent>()) | |
| { | |
| return c->as<LayoutComponent>(); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| bool LayoutParticipant::isParticipatingInLayout() const | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| return m_layoutData != nullptr; | |
| #else | |
| return false; | |
| #endif | |
| } | |
| StatusCode LayoutParticipant::onAddedClean(CoreContext* context) | |
| { | |
| StatusCode code = Super::onAddedClean(context); | |
| if (code != StatusCode::Ok) | |
| { | |
| return code; | |
| } | |
| #ifdef WITH_RIVE_LAYOUT | |
| resync(); | |
| #endif | |
| return StatusCode::Ok; | |
| } | |
| #ifdef WITH_RIVE_LAYOUT | |
| void LayoutParticipant::releaseLayoutData() | |
| { | |
| if (m_layoutData == nullptr) | |
| { | |
| return; | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| m_layoutData->unref(); | |
| #else | |
| delete m_layoutData; | |
| #endif | |
| m_layoutData = nullptr; | |
| } | |
| // Releases a fill axis from any content-based minimum, guarding against CSS's | |
| // min-width:auto. Yoga has no such minimum today, so this is currently inert — | |
| // see layout_fill_content_floor_test.dart, which fails if that changes. | |
| // LayoutComponent has no equivalent. | |
| // | |
| // Overrides rather than sits inline so it lands after the min/max Super writes. | |
| void LayoutParticipant::applyBaseStyle(YGStyle& style, | |
| const LayoutSyncContext& context) | |
| { | |
| LayoutSizingStyle::applyBaseStyle(style, context); | |
| const LayoutScaleType widthScale = (LayoutScaleType)layoutWidthScaleType(); | |
| const LayoutScaleType heightScale = | |
| (LayoutScaleType)layoutHeightScaleType(); | |
| const bool parentIsRow = context.parentIsRow; | |
| const bool parentIsGridLike = context.parentIsGrid; | |
| const bool widthFill = widthScale == LayoutScaleType::fill; | |
| const bool heightFill = heightScale == LayoutScaleType::fill; | |
| style.dimensions()[YGDimensionWidth] = | |
| widthScale == LayoutScaleType::fixed | |
| ? YGValue{std::max(0.0f, width()), (YGUnit)widthUnitsValue()} | |
| : YGValue{YGUndefined, YGUnitAuto}; | |
| style.dimensions()[YGDimensionHeight] = | |
| heightScale == LayoutScaleType::fixed | |
| ? YGValue{std::max(0.0f, height()), (YGUnit)heightUnitsValue()} | |
| : YGValue{YGUndefined, YGUnitAuto}; | |
| if (parentIsGridLike) | |
| { | |
| style.flexGrow() = YGFloatOptional(0.0f); | |
| style.flexShrink() = YGFloatOptional(0.0f); | |
| style.alignSelf() = heightFill ? YGAlignStretch : YGAlignAuto; | |
| } | |
| else | |
| { | |
| bool mainFill = parentIsRow ? widthFill : heightFill; | |
| float mainFraction = | |
| parentIsRow ? fractionalWidth() : fractionalHeight(); | |
| style.flexGrow() = YGFloatOptional(mainFill ? mainFraction : 0.0f); | |
| style.flexShrink() = YGFloatOptional(mainFill ? mainFraction : 0.0f); | |
| style.flexBasis() = mainFill ? YGValue{0.0f, YGUnitPoint} | |
| : YGValue{YGUndefined, YGUnitAuto}; | |
| bool crossFill = parentIsRow ? heightFill : widthFill; | |
| style.alignSelf() = crossFill ? YGAlignStretch : YGAlignAuto; | |
| } | |
| if (layoutWidthScaleType() == (uint32_t)LayoutScaleType::fill && | |
| (YGUnit)minWidthUnitsValue() == YGUnitUndefined) | |
| { | |
| style.minDimensions()[YGDimensionWidth] = YGValue{0.0f, YGUnitPoint}; | |
| } | |
| if (layoutHeightScaleType() == (uint32_t)LayoutScaleType::fill && | |
| (YGUnit)minHeightUnitsValue() == YGUnitUndefined) | |
| { | |
| style.minDimensions()[YGDimensionHeight] = YGValue{0.0f, YGUnitPoint}; | |
| } | |
| } | |
| void LayoutParticipant::resync() | |
| { | |
| auto* host = transformComponent(); | |
| if (host == nullptr) | |
| { | |
| return; | |
| } | |
| if (m_layoutData == nullptr) | |
| { | |
| m_layoutData = new LayoutData(); | |
| m_layoutData->node.getConfig()->setPointScaleFactor(0); | |
| // Measure our host's intrinsic (hug) size via IntrinsicallySizeable. | |
| m_layoutData->node.setContext(host); | |
| m_layoutData->node.setMeasureFunc(participantMeasureFunc); | |
| // We are our own sizing style. | |
| addLayoutStyleApplier(this); | |
| } | |
| // A sibling placement that cleaned before us was dropped on the floor: | |
| // addLayoutStyleApplier is a no-op until m_layoutData exists, and | |
| // onAddedClean runs in file order, so nothing fixes the order for us. | |
| // Idempotent — appliers are pushed unique. | |
| if (auto* placement = GridItemPlacement::from(host)) | |
| { | |
| addLayoutStyleApplier(placement); | |
| } | |
| syncStyleChanges(); | |
| if (auto* lc = owningLayout()) | |
| { | |
| lc->syncLayoutChildren(); | |
| } | |
| host->addDirt(ComponentDirt::WorldTransform, true); | |
| markLayoutNodeDirty(true); | |
| } | |
| void* LayoutParticipant::layoutNode(int index) | |
| { | |
| return m_layoutData != nullptr ? static_cast<void*>(&m_layoutData->node) | |
| : nullptr; | |
| } | |
| Layout LayoutParticipant::solvedLayout() const | |
| { | |
| if (m_layoutData == nullptr) | |
| { | |
| return Layout(); | |
| } | |
| const auto& l = m_layoutData->node.getLayout(); | |
| return Layout(definedOrZero(l.position[YGEdgeLeft]), | |
| definedOrZero(l.position[YGEdgeTop]), | |
| definedOrZero(l.dimensions[YGDimensionWidth]), | |
| definedOrZero(l.dimensions[YGDimensionHeight])); | |
| } | |
| // While animating, the resolved slot is the interpolated animatedLayout; | |
| // otherwise it's read straight from the yoga node (no per-participant cache). | |
| float LayoutParticipant::resolvedLeft() const | |
| { | |
| return m_animation != nullptr ? m_animation->animatedLayout.left() | |
| : solvedLayout().left(); | |
| } | |
| float LayoutParticipant::resolvedTop() const | |
| { | |
| return m_animation != nullptr ? m_animation->animatedLayout.top() | |
| : solvedLayout().top(); | |
| } | |
| float LayoutParticipant::resolvedWidth() const | |
| { | |
| return m_animation != nullptr ? m_animation->animatedLayout.width() | |
| : solvedLayout().width(); | |
| } | |
| float LayoutParticipant::resolvedHeight() const | |
| { | |
| return m_animation != nullptr ? m_animation->animatedLayout.height() | |
| : solvedLayout().height(); | |
| } | |
| void LayoutParticipant::applyResolvedLayoutSize() | |
| { | |
| auto* sizeable = IntrinsicallySizeable::from(transformComponent()); | |
| if (sizeable == nullptr) | |
| { | |
| return; | |
| } | |
| auto* lc = owningLayout(); | |
| LayoutDirection direction = | |
| lc != nullptr ? lc->actualDirection() : LayoutDirection::inherit; | |
| // Read the resolved slot once (animated slot, or a single yoga-node read) | |
| // rather than calling resolvedWidth()/resolvedHeight() separately. | |
| const Layout resolved = | |
| m_animation != nullptr ? m_animation->animatedLayout : solvedLayout(); | |
| sizeable->controlSize(Vec2D(resolved.width(), resolved.height()), | |
| (LayoutScaleType)layoutWidthScaleType(), | |
| (LayoutScaleType)layoutHeightScaleType(), | |
| direction); | |
| } | |
| #else | |
| float LayoutParticipant::resolvedLeft() const { return 0.0f; } | |
| float LayoutParticipant::resolvedTop() const { return 0.0f; } | |
| float LayoutParticipant::resolvedWidth() const { return 0.0f; } | |
| float LayoutParticipant::resolvedHeight() const { return 0.0f; } | |
| #endif | |
| size_t LayoutParticipant::numLayoutNodes() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| return m_layoutData != nullptr ? 1 : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| AABB LayoutParticipant::layoutBounds() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| // Read the resolved slot once (animated slot, or a single yoga-node read) | |
| // instead of resolvedLeft/Top/Width/Height each re-reading it. | |
| const Layout resolved = | |
| m_animation != nullptr ? m_animation->animatedLayout : solvedLayout(); | |
| return AABB::fromLTWH(resolved.left(), | |
| resolved.top(), | |
| resolved.width(), | |
| resolved.height()); | |
| #else | |
| return AABB::fromLTWH(0.0f, 0.0f, 0.0f, 0.0f); | |
| #endif | |
| } | |
| AABB LayoutParticipant::layoutBoundsForNode(int index) | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| #endif | |
| return layoutBounds(); | |
| } | |
| bool LayoutParticipant::syncStyleChanges() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (m_layoutData == nullptr) | |
| { | |
| return false; | |
| } | |
| YGNode& node = m_layoutData->node; | |
| // Appliers write straight into the node's own style; no scratch copy. | |
| YGStyle& ygStyle = m_layoutData->style(); | |
| LayoutScaleType widthScale = (LayoutScaleType)layoutWidthScaleType(); | |
| LayoutScaleType heightScale = (LayoutScaleType)layoutHeightScaleType(); | |
| auto* lc = owningLayout(); | |
| bool parentIsRow = lc != nullptr ? lc->mainAxisIsRow() : true; | |
| bool parentIsGridLike = | |
| lc != nullptr && lc->style() != nullptr && lc->style()->isGrid(); | |
| bool needsMeasure = widthScale == LayoutScaleType::hug || | |
| heightScale == LayoutScaleType::hug; | |
| if (needsMeasure) | |
| { | |
| node.setContext(transformComponent()); | |
| node.setMeasureFunc(participantMeasureFunc); | |
| } | |
| else | |
| { | |
| node.setMeasureFunc(nullptr); | |
| } | |
| bool parentIsStack = | |
| lc != nullptr && lc->style() != nullptr && lc->style()->isStack(); | |
| uint32_t containerJustifyItems = (lc != nullptr && lc->style() != nullptr) | |
| ? lc->style()->justifyItemsValue() | |
| : (uint32_t)YGJustifyStretch; | |
| // Appliers last, before the style reaches the node. A participant's | |
| // GridItemPlacement hangs off the same Node that owns this participant. | |
| LayoutSyncContext syncContext; | |
| syncContext.parentIsGrid = parentIsGridLike; | |
| syncContext.parentIsStack = parentIsStack; | |
| syncContext.containerJustifyItems = containerJustifyItems; | |
| syncContext.inlineHugs = widthScale == LayoutScaleType::hug; | |
| syncContext.widthFills = widthScale == LayoutScaleType::fill; | |
| syncContext.parentIsRow = parentIsRow; | |
| syncContext.isLTR = | |
| lc == nullptr || lc->actualDirection() != LayoutDirection::rtl; | |
| syncContext.hasLayoutParent = lc != nullptr; | |
| m_layoutData->applyLayoutStyles(ygStyle, syncContext); | |
| node.markDirtyAndPropagate(); | |
| // Fold display:none into the host's collapse so it stops drawing (it's | |
| // already removed from the layout flow via the yoga display above). | |
| if (auto* host = transformComponent()) | |
| { | |
| auto* p = host->parent(); | |
| bool parentHidesHost = p != nullptr && p->isCollapsed(); | |
| // A Solo hides its non-active children; mirror that here so folding our | |
| // display doesn't reveal an inactive Solo child. (Dart re-dispatches | |
| // through the parent's collapse; C++ has no host mixin, so we check | |
| // it.) | |
| if (p != nullptr && p->is<Solo>()) | |
| { | |
| auto* solo = p->as<Solo>(); | |
| auto* ab = solo->artboard(); | |
| Core* active = ab != nullptr | |
| ? ab->resolve(solo->activeComponentId()) | |
| : nullptr; | |
| if (active != host) | |
| { | |
| parentHidesHost = true; | |
| } | |
| } | |
| host->collapse(parentHidesHost || | |
| (YGDisplay)displayValue() == YGDisplayNone); | |
| } | |
| return true; | |
| #else | |
| return false; | |
| #endif | |
| } | |
| void LayoutParticipant::updateLayoutBounds(bool animate) | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (m_layoutData == nullptr) | |
| { | |
| return; | |
| } | |
| YGNode& node = m_layoutData->node; | |
| if (!node.getHasNewLayout()) | |
| { | |
| return; | |
| } | |
| node.setHasNewLayout(false); | |
| Layout newLayout = solvedLayout(); | |
| // Animate only when under an animated layout (m_animation allocated), the | |
| // animate flag is set, and we've solved before (so we snap on first | |
| // appearance instead of animating in from 0,0). | |
| if (m_animation != nullptr && animate && m_hasSolvedLayout) | |
| { | |
| // Retarget: animate from where we are now to the newly solved slot, | |
| // smoothing over any in-flight animation (mirrors LayoutComponent). | |
| auto* animationData = currentAnimationData(); | |
| if (newLayout != animationData->to) | |
| { | |
| if (animationData->elapsedSeconds != 0.0f) | |
| { | |
| if (m_animation->isSmoothing) | |
| { | |
| m_animation->a.copy(m_animation->b); | |
| } | |
| m_animation->isSmoothing = true; | |
| } | |
| else | |
| { | |
| m_animation->isSmoothing = false; | |
| } | |
| animationData = currentAnimationData(); | |
| animationData->from = m_animation->animatedLayout; | |
| animationData->to = newLayout; | |
| animationData->elapsedSeconds = 0.0f; | |
| } | |
| } | |
| else if (m_animation != nullptr) | |
| { | |
| // Snap the animated slot (first solve, or the animate flag is off). | |
| m_animation->animatedLayout = newLayout; | |
| m_animation->a.to = newLayout; | |
| } | |
| // else: not animating — resolvedLeft etc. read the yoga node directly. | |
| m_hasSolvedLayout = true; | |
| applyResolvedLayoutSize(); | |
| if (auto* host = transformComponent()) | |
| { | |
| host->addDirt(ComponentDirt::WorldTransform, true); | |
| } | |
| #endif | |
| } | |
| void LayoutParticipant::markLayoutNodeDirty(bool shouldForceUpdateLayoutBounds) | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (m_layoutData != nullptr) | |
| { | |
| m_layoutData->node.markDirtyAndPropagate(); | |
| } | |
| if (auto* lc = owningLayout()) | |
| { | |
| lc->markLayoutNodeDirty(shouldForceUpdateLayoutBounds); | |
| } | |
| #endif | |
| } | |
| void LayoutParticipant::onSizingChanged() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| syncStyleChanges(); | |
| markLayoutNodeDirty(); | |
| #endif | |
| } | |
| // ── Layout animation. The participant has no animation style of its own; it | |
| // inherits the parent layout's (stored via cascadeLayoutStyle) and interpolates | |
| // its resolved slot toward each newly solved layout, advanced each frame as an | |
| // AdvancingComponent. | |
| LayoutAnimationData* LayoutParticipant::currentAnimationData() | |
| { | |
| // Only called while animating (m_animation != nullptr). | |
| return m_animation->isSmoothing ? &m_animation->b : &m_animation->a; | |
| } | |
| bool LayoutParticipant::animates() const { return m_animation != nullptr; } | |
| LayoutStyleInterpolation LayoutParticipant::interpolation() const | |
| { | |
| return m_animation != nullptr ? m_animation->interpolation | |
| : LayoutStyleInterpolation::hold; | |
| } | |
| float LayoutParticipant::interpolationTime() const | |
| { | |
| return m_animation != nullptr ? m_animation->interpolationTime : 0.0f; | |
| } | |
| KeyFrameInterpolator* LayoutParticipant::interpolator() const | |
| { | |
| return m_animation != nullptr ? m_animation->interpolator : nullptr; | |
| } | |
| bool LayoutParticipant::advanceComponent(float elapsedSeconds, | |
| AdvanceFlags flags) | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (m_animation == nullptr || | |
| (flags & AdvanceFlags::NewFrame) != AdvanceFlags::NewFrame) | |
| { | |
| return false; | |
| } | |
| return applyInterpolation(elapsedSeconds, | |
| (flags & AdvanceFlags::Animate) == | |
| AdvanceFlags::Animate || | |
| (flags & AdvanceFlags::AdvanceNested) == | |
| AdvanceFlags::AdvanceNested); | |
| #else | |
| return false; | |
| #endif | |
| } | |
| #ifdef WITH_RIVE_LAYOUT | |
| bool LayoutParticipant::cascadeLayoutStyle( | |
| LayoutStyleInterpolation inheritedInterpolation, | |
| KeyFrameInterpolator* inheritedInterpolator, | |
| float inheritedInterpolationTime, | |
| LayoutDirection direction) | |
| { | |
| // A participant has no animation style of its own; it inherits the parent | |
| // layout's. Allocate the animation state only while it actually animates. | |
| bool willAnimate = | |
| inheritedInterpolation != LayoutStyleInterpolation::hold && | |
| inheritedInterpolationTime > 0.0f; | |
| if (willAnimate) | |
| { | |
| if (m_animation == nullptr) | |
| { | |
| m_animation = new ParticipantAnimation(); | |
| // Seed from the current resolved slot so enabling animation | |
| // mid-life doesn't animate in from 0. | |
| Layout current = solvedLayout(); | |
| m_animation->animatedLayout = current; | |
| m_animation->a.from = current; | |
| m_animation->a.to = current; | |
| } | |
| m_animation->interpolation = inheritedInterpolation; | |
| m_animation->interpolator = inheritedInterpolator; | |
| m_animation->interpolationTime = inheritedInterpolationTime; | |
| } | |
| else if (m_animation != nullptr) | |
| { | |
| // Parent no longer animates: drop the state and snap from here on. | |
| delete m_animation; | |
| m_animation = nullptr; | |
| } | |
| return willAnimate; | |
| } | |
| bool LayoutParticipant::applyInterpolation(float elapsedSeconds, bool animate) | |
| { | |
| if (m_animation == nullptr) | |
| { | |
| return false; | |
| } | |
| auto* animationData = currentAnimationData(); | |
| if (!animate || animationData->to == m_animation->animatedLayout) | |
| { | |
| return false; | |
| } | |
| if (m_animation->isSmoothing) | |
| { | |
| float f = | |
| std::fmin(1.0f, | |
| interpolationTime() > 0.0f | |
| ? m_animation->a.elapsedSeconds / interpolationTime() | |
| : 1.0f); | |
| if (interpolation() != LayoutStyleInterpolation::linear && | |
| interpolator() != nullptr) | |
| { | |
| f = interpolator()->transform(f); | |
| } | |
| m_animation->b.from = m_animation->a.interpolate(f); | |
| if (f == 1.0f) | |
| { | |
| m_animation->a.copy(m_animation->b); | |
| m_animation->isSmoothing = false; | |
| } | |
| else | |
| { | |
| m_animation->a.elapsedSeconds += elapsedSeconds; | |
| } | |
| } | |
| animationData = currentAnimationData(); | |
| if (animationData->elapsedSeconds >= interpolationTime()) | |
| { | |
| m_animation->animatedLayout = animationData->to; | |
| if (m_animation->isSmoothing) | |
| { | |
| m_animation->isSmoothing = false; | |
| m_animation->a.copy(m_animation->b); | |
| m_animation->a.elapsedSeconds = m_animation->b.elapsedSeconds = | |
| 0.0f; | |
| } | |
| else | |
| { | |
| m_animation->a.elapsedSeconds = 0.0f; | |
| } | |
| applyResolvedLayoutSize(); | |
| if (auto* host = transformComponent()) | |
| { | |
| host->addDirt(ComponentDirt::WorldTransform, true); | |
| } | |
| return false; | |
| } | |
| float f = | |
| std::fmin(1.0f, | |
| interpolationTime() > 0.0f | |
| ? animationData->elapsedSeconds / interpolationTime() | |
| : 1.0f); | |
| if (interpolation() != LayoutStyleInterpolation::linear && | |
| interpolator() != nullptr) | |
| { | |
| f = interpolator()->transform(f); | |
| } | |
| auto current = animationData->interpolate(f); | |
| if (m_animation->animatedLayout != current) | |
| { | |
| m_animation->animatedLayout = current; | |
| applyResolvedLayoutSize(); | |
| if (auto* host = transformComponent()) | |
| { | |
| host->addDirt(ComponentDirt::WorldTransform, true); | |
| } | |
| } | |
| animationData->elapsedSeconds += elapsedSeconds; | |
| return f != 1.0f; | |
| } | |
| #endif |