/* osgEarth
* Copyright 2025 Pelican Mapping
* MIT License
*/
#pragma once

#include <osgEarth/ScreenSpaceLayoutImpl>
#include <osgEarth/CameraUtils>
#include <osgEarth/Math>
#include <osgText/Text>

#undef FADE_UNIFORM_NAME
#define FADE_UNIFORM_NAME "oe_declutter_fade"

namespace osgEarth { namespace Internal
{
    using namespace osgEarth;

    static const char* s_faderFS =
        "uniform float " FADE_UNIFORM_NAME ";\n"
        "void oe_declutter_apply_fade(inout vec4 color) { \n"
        "    color.a *= " FADE_UNIFORM_NAME ";\n"
        "}\n";

    // records information about each drawable.
    // TODO: a way to clear out this list when drawables go away
    struct DrawableInfo
    {
        DrawableInfo() : _lastAlpha(1.0f), _lastScale(1.0f), _frame(0u), _visible(true) { }
        float _lastAlpha, _lastScale;
        unsigned _frame;
        bool _visible;
    };

    using DrawableMemory = std::unordered_map<const osg::Drawable*, DrawableInfo>;

    typedef std::pair<const osg::Node*, osg::BoundingBox> RenderLeafBox;

    // Data structure stored one-per-View.
    struct PerCamInfo
    {
        PerCamInfo() : _lastTimeStamp(0), _firstFrame(true) { }

        // remembers the state of each drawable from the previous pass
        DrawableMemory _memory;

        // re-usable structures (to avoid unnecessary re-allocation)
        osgUtil::RenderBin::RenderLeafList _passed;
        osgUtil::RenderBin::RenderLeafList _failed;
        std::vector<RenderLeafBox>         _used;

        // time stamp of the previous pass, for calculating animation speed
        osg::Timer_t _lastTimeStamp;
        bool _firstFrame;
        osg::Matrix _lastCamVPW;
    };

    /**
    * A custom RenderLeaf sorting algorithm for decluttering objects.
    *
    * First we sort the leaves front-to-back so that objects closer to the camera
    * get higher priority. If you have installed a custom sorting functor,
    * this is used instead.
    *
    * Next, we go though all the drawables and remove any that try to occupy
    * already-occupied real estate in the 2D viewport. Objects that fail the test
    * go on a "failed" list and are either completely removed from the display
    * or transitioned to a secondary visual state (scaled down, alpha'd down)
    * dependeing on the options setup.
    *
    * Drawables with the same parent (i.e., Geode) are treated as a group. As
    * soon as one passes the occlusion test, all its siblings will automatically
    * pass as well.
    */
    struct /*internal*/ DeclutterImplementation : public osgUtil::RenderBin::SortCallback
    {
        DeclutterSortFunctor* _customSortFunctor;
        ScreenSpaceLayoutContext* _context;

        PerObjectFastMap<osg::Camera*, PerCamInfo> _perCam;

        /**
        * Constructs the new sorter.
        * @param f Custom declutter sorting predicate. Pass NULL to use the
        *          default sorter (sort by distance-to-camera).
        */
        DeclutterImplementation( ScreenSpaceLayoutContext* context, DeclutterSortFunctor* f = 0L )
            : _context(context), _customSortFunctor(f)
        {
            //nop
        }

        // override.
        // Sorts the bin. This runs in the CULL thread after the CULL traversal has completed.
        void sortImplementation(osgUtil::RenderBin* bin)
        {
            const ScreenSpaceLayoutOptions& options = _context->_options;

            osgUtil::RenderBin::RenderLeafList& leaves = bin->getRenderLeafList();

            bin->copyLeavesFromStateGraphListToRenderLeafList();

            // first, sort the leaves:
            if ( _customSortFunctor && ScreenSpaceLayout::globallyEnabled )
            {
                // if there's a custom sorting function installed
                std::sort( leaves.begin(), leaves.end(), SortContainer( *_customSortFunctor ) );
            }
            else if (options.sortByDistance() == true)
            {
                // default behavior:
                std::sort( leaves.begin(), leaves.end(), SortFrontToBackPreservingGeodeTraversalOrder() );
            }

            // nothing to sort? bail out
            if ( leaves.size() == 0 )
                return;

            // access the view-specific persistent data:
            osg::Camera* cam = bin->getStage()->getCamera();

            // bail out if this camera is a master camera with no GC
            // (e.g., in a multi-screen layout)
            if (cam == NULL || (cam->getGraphicsContext() == NULL && !cam->isRenderToTextureCamera()))
            {
                return;
            }

            PerCamInfo& local = _perCam.get( cam );

            osg::Timer_t now = osg::Timer::instance()->tick();
            if (local._firstFrame)
            {
                local._firstFrame = false;
                local._lastTimeStamp = now;
            }

            // calculate the elapsed time since the previous pass; we'll use this for
            // the animations
            float elapsedSeconds = osg::Timer::instance()->delta_s(local._lastTimeStamp, now);
            local._lastTimeStamp = now;

            // Reset the local re-usable containers
            local._passed.clear();          // drawables that pass occlusion test
            local._failed.clear();          // drawables that fail occlusion test
            local._used.clear();            // list of occupied bounding boxes in screen space

                                            // compute a window matrix so we can do window-space culling. If this is an RTT camera
                                            // with a reference camera attachment, we actually want to declutter in the window-space
                                            // of the reference camera. (e.g., for picking).
            const osg::Viewport* vp = cam->getViewport();

            osg::Matrix windowMatrix = vp->computeWindowMatrix();

            osg::Vec3f  refCamScale(1.0f, 1.0f, 1.0f);
            osg::Matrix refCamScaleMat;
            osg::Matrix refWindowMatrix = windowMatrix;

            // If the camera is actually an RTT slave camera, it's our picker, and we need to
            // adjust the scale to match it.
            if (CameraUtils::isPickCamera(cam) && cam->getView() && cam->getView()->getCamera())
            {
                osg::Camera* parentCam = cam->getView()->getCamera();
                const osg::Viewport* refVP = parentCam->getViewport();
                refCamScale.set( vp->width() / refVP->width(), vp->height() / refVP->height(), 1.0 );
                refCamScaleMat.makeScale( refCamScale );
                refWindowMatrix = refVP->computeWindowMatrix();
            }
            else
            {
                // adjust for DPR:
                auto dpr = Registry::instance()->getDevicePixelRatio(cam->getGraphicsContext());
                refCamScaleMat.makeScale(dpr, dpr, 1.0);
            }

            // Track the parent nodes of drawables that are obscured (and culled). Drawables
            // with the same parent node (typically a Geode) are considered to be grouped and
            // will be culled as a group.
            std::set<const osg::Node*> culledParents;

            unsigned limit = *options.maxObjects();

            bool snapToPixel = options.snapToPixel() == true;

            osg::Matrix camVPW;
            camVPW.postMult(cam->getViewMatrix());
            camVPW.postMult(cam->getProjectionMatrix());
            camVPW.postMult(refWindowMatrix);

            // has the camera moved?
            bool camChanged = camVPW != local._lastCamVPW;
            local._lastCamVPW = camVPW;

            std::unordered_set<std::string> uniqueText;

            // Go through each leaf and test for visibility.
            // Enforce the "max objects" limit along the way.
            for(osgUtil::RenderBin::RenderLeafList::iterator i = leaves.begin();
                i != leaves.end() && local._passed.size() < limit;
                ++i )
            {
                bool visible = true;

                osgUtil::RenderLeaf* leaf = *i;
                const osg::Drawable* drawable = leaf->getDrawable();
                const osg::Node*     drawableParent = drawable->getNumParents()? drawable->getParent(0) : 0L;

                const ScreenSpaceLayoutData* layoutData = dynamic_cast<const ScreenSpaceLayoutData*>(drawable->getUserData());

                // transform the bounding box of the drawable into window-space.
                osg::BoundingBox box = drawable->getBoundingBox();

                osg::Vec3f offset;
                osg::Quat rot;

                if (layoutData)
                {
                    // local transformation data
                    // and management of the label orientation (must be always readable)

                    bool isText = dynamic_cast<const osgText::Text*>(drawable) != 0L;

                    float angle = 0.0f;
                    if (layoutData->getRotationDegrees() != 0.0f)
                    {
                        angle = deg2rad(layoutData->getRotationDegrees());
                    }
                    else
                    {
                        osg::Vec3d loc = layoutData->getAnchorPoint() * camVPW;
                        osg::Vec3d proj = layoutData->getProjPoint() * camVPW;
                        proj -= loc;
                        angle = atan2(proj.y(), proj.x());
                    }

                    if ( isText && (angle < -osg::PI_2 || angle > osg::PI_2) )
                    {
                        // avoid the label characters to be inverted:
                        // use a symetric translation and adapt the rotation to be in the desired angles
                        offset.set( -layoutData->_pixelOffset.x() - box.xMax() - box.xMin(),
                            -layoutData->_pixelOffset.y() - box.yMax() - box.yMin(),
                            0.f );
                        angle += angle < -osg::PI_2? osg::PI : -osg::PI; // JD #1029
                    }
                    else
                    {
                        offset.set( layoutData->_pixelOffset.x(), layoutData->_pixelOffset.y(), 0.f );
                    }

                    // handle the local rotation
                    if ( angle != 0.f )
                    {
                        rot.makeRotate ( angle, osg::Vec3d(0, 0, 1) );
                        osg::Vec3f ld = rot * ( osg::Vec3f(box.xMin(), box.yMin(), 0.) );
                        osg::Vec3f lu = rot * ( osg::Vec3f(box.xMin(), box.yMax(), 0.) );
                        osg::Vec3f ru = rot * ( osg::Vec3f(box.xMax(), box.yMax(), 0.) );
                        osg::Vec3f rd = rot * ( osg::Vec3f(box.xMax(), box.yMin(), 0.) );
                        if ( angle > - osg::PI / 2. && angle < osg::PI / 2.)
                            box.set( osg::minimum(ld.x(), lu.x()), osg::minimum(ld.y(), rd.y()), 0,
                                osg::maximum(rd.x(), ru.x()), osg::maximum(lu.y(), ru.y()), 0 );
                        else
                            box.set( osg::minimum(ld.x(), lu.x()), osg::minimum(lu.y(), ru.y()), 0,
                                osg::maximum(ld.x(), lu.x()), osg::maximum(ld.y(), rd.y()), 0 );
                    }

                    // scale and offset the box, taking DPR into account.
                    double hw = 0.5 * (box.xMax() - box.xMin());
                    double hh = 0.5 * (box.yMax() - box.yMin());
                    box.xMin() -= (hw * (refCamScaleMat(0, 0) - 1.0));
                    box.xMax() += (hw * (refCamScaleMat(0, 0) - 1.0));
                    box.yMin() -= (hh * (refCamScaleMat(1, 1) - 1.0));
                    box.yMax() += (hh * (refCamScaleMat(1, 1) - 1.0));

                    offset = refCamScaleMat * offset;

                    // handle the local translation
                    box.xMin() += offset.x();
                    box.xMax() += offset.x();
                    box.yMin() += offset.y();
                    box.yMax() += offset.y();
                }

                static osg::Vec4d s_zero_w(0,0,0,1);
                osg::Matrix MVP = (*leaf->_modelview.get()) * (*leaf->_projection.get());
                osg::Vec4d clip = s_zero_w * MVP;
                osg::Vec3d clip_ndc( clip.x()/clip.w(), clip.y()/clip.w(), clip.z()/clip.w() );

                // if we are using a reference camera (like for picking), we do the decluttering in
                // its viewport so that they match.
                osg::Vec3f winPos    = clip_ndc * windowMatrix;
                osg::Vec3f refWinPos = clip_ndc * refWindowMatrix;

                // Expand the box if this object is currently not visible, so that it takes a little
                // more room for it to before visible once again.
                DrawableInfo& info = local._memory[drawable];
                float buffer = info._visible ? 1.0f : 3.0f;

                // The "declutter" box is the box we use to reserve screen space.
                // This must be unquantized regardless of whether snapToPixel is set.
                box.set(
                    floor(refWinPos.x() + box.xMin())-buffer,
                    floor(refWinPos.y() + box.yMin())-buffer,
                    refWinPos.z(),
                    ceil(refWinPos.x() + box.xMax())+buffer,
                    ceil(refWinPos.y() + box.yMax())+buffer,
                    refWinPos.z() );

                // if snapping is enabled, only snap when the camera stops moving.
                bool quantize = snapToPixel;
                if ( quantize && !camChanged )
                {
                    // Quanitize the window draw coordinates to mitigate text rendering filtering anomalies.
                    // Drawing text glyphs on pixel boundaries mitigates aliasing.
                    // Adding 0.5 will cause the GPU to sample the glyph texels exactly on center.
                    winPos.x() = floor(winPos.x()) + 0.5;
                    winPos.y() = floor(winPos.y()) + 0.5;
                }

                if ( ScreenSpaceLayout::globallyEnabled )
                {
                    // A max priority => never occlude.
                    float priority = layoutData ? layoutData->_priority : 0.0f;

                    if ( priority == FLT_MAX )
                    {
                        visible = true;
                    }

                    // if this leaf is already in a culled group, skip it.
                    else if ( drawableParent != 0L && culledParents.find(drawableParent) != culledParents.end() )
                    {
                        visible = false;
                    }

                    else
                    {
                        // weed out any drawables that are obscured by closer drawables.
                        // TODO: think about a more efficient algorithm - right now we are just using
                        // brute force to compare all bbox's
                        for( std::vector<RenderLeafBox>::const_iterator j = local._used.begin(); j != local._used.end(); ++j )
                        {
                            // only need a 2D test since we're in clip space
                            bool isClear =
                                box.xMin() > j->second.xMax() ||
                                box.xMax() < j->second.xMin() ||
                                box.yMin() > j->second.yMax() ||
                                box.yMax() < j->second.yMin();

                            // if there's an overlap (and the conflict isn't from the same drawable
                            // parent, which is acceptable), then the leaf is culled.
                            if ( !isClear && drawableParent != j->first )
                            {
                                visible = false;
                                break;
                            }
                        }
                    }
                }

                if (visible && !drawable->getName().empty())
                {
                    auto r = uniqueText.emplace(drawable->getName());

                    if (layoutData && layoutData->_unique && r.second == false)
                    {
                        visible = false;
                    }
                }

                if ( visible )
                {
                    // passed the test, so add the leaf's bbox to the "used" list, and add the leaf
                    // to the final draw list.
                    if (drawableParent)
                        local._used.push_back( std::make_pair(drawableParent, box) );

                    local._passed.push_back( leaf );
                }

                else
                {
                    // culled, so put the parent in the parents list so that any future leaves
                    // with the same parent will be trivially rejected
                    if (drawableParent)
                        culledParents.insert(drawableParent);

                    local._failed.push_back( leaf );
                }

                // modify the leaf's modelview matrix to correctly position it in the 2D ortho
                // projection when it's drawn later. We'll also preserve the scale.
                osg::Matrix newModelView;
                if ( rot.zeroRotation() )
                {
                    newModelView.makeTranslate( osg::Vec3f(winPos.x() + offset.x(), winPos.y() + offset.y(), 0) );
                    newModelView.preMultScale( leaf->_modelview->getScale() * refCamScaleMat );
                }
                else
                {
                    offset = rot * offset;
                    newModelView.makeTranslate( osg::Vec3f(winPos.x() + offset.x(), winPos.y() + offset.y(), 0) );
                    newModelView.preMultScale( leaf->_modelview->getScale() * refCamScaleMat );
                    newModelView.preMultRotate( rot );
                }

                // Leaf modelview matrixes are shared (by objects in the traversal stack) so we
                // cannot just replace it unfortunately. Have to make a new one. Perhaps a nice
                // allocation pool is in order here
                leaf->_modelview = new osg::RefMatrix( newModelView );
            }

            // copy the final draw list back into the bin, rejecting any leaves whose parents
            // are in the cull list.
            if ( ScreenSpaceLayout::globallyEnabled )
            {
                leaves.clear();
                for( osgUtil::RenderBin::RenderLeafList::const_iterator i=local._passed.begin(); i != local._passed.end(); ++i )
                {
                    osgUtil::RenderLeaf* leaf     = *i;
                    const osg::Drawable* drawable = leaf->getDrawable();
                    const osg::Node* drawableParent = drawable->getNumParents() > 0 ? drawable->getParent(0) : 0L;

                    if ( drawableParent == 0L || culledParents.find(drawableParent) == culledParents.end() )
                    {
                        DrawableInfo& info = local._memory[drawable];

                        bool fullyIn = true;

                        // scale in until at full scale:
                        if ( info._lastScale != 1.0f )
                        {
                            fullyIn = false;
                            info._lastScale += elapsedSeconds / osg::maximum(*options.inAnimationTime(), 0.001f);
                            if ( info._lastScale > 1.0f )
                                info._lastScale = 1.0f;
                        }

                        if ( info._lastScale != 1.0f )
                            leaf->_modelview->preMult( osg::Matrix::scale(info._lastScale,info._lastScale,1) );

                        // fade in until at full alpha:
                        if ( info._lastAlpha != 1.0f )
                        {
                            fullyIn = false;
                            info._lastAlpha += elapsedSeconds / osg::maximum(*options.inAnimationTime(), 0.001f);
                            if ( info._lastAlpha > 1.0f )
                                info._lastAlpha = 1.0f;
                        }

                        leaf->_depth = info._lastAlpha;
                        leaves.push_back( leaf );

                        info._frame++;
                        info._visible = true;
                    }
                    else
                    {
                        local._failed.push_back(leaf);
                    }
                }

                // next, go through the FAILED list and sort them into failure bins so we can draw
                // them using a different technique if necessary.
                for( osgUtil::RenderBin::RenderLeafList::const_iterator i=local._failed.begin(); i != local._failed.end(); ++i )
                {
                    osgUtil::RenderLeaf* leaf =     *i;
                    const osg::Drawable* drawable = leaf->getDrawable();

                    DrawableInfo& info = local._memory[drawable];

                    bool isText = dynamic_cast<const osgText::Text*>(drawable) != 0L;
                    bool isBbox = drawable && (strcmp(drawable->className(), "BboxDrawable") == 0);
                    bool fullyOut = true;

                    if (info._frame > 0u)
                    {
                        if ( info._lastScale != *options.minAnimationScale() )
                        {
                            fullyOut = false;
                            info._lastScale -= elapsedSeconds / osg::maximum(*options.outAnimationTime(), 0.001f);
                            if ( info._lastScale < *options.minAnimationScale() )
                                info._lastScale = *options.minAnimationScale();
                        }

                        if ( info._lastAlpha != *options.minAnimationAlpha() )
                        {
                            fullyOut = false;
                            info._lastAlpha -= elapsedSeconds / osg::maximum(*options.outAnimationTime(), 0.001f);
                            if ( info._lastAlpha < *options.minAnimationAlpha() )
                                info._lastAlpha = *options.minAnimationAlpha();
                        }
                    }
                    else
                    {
                        // prevent first-frame "pop out"
                        info._lastScale = options.minAnimationScale().get();
                        info._lastAlpha = options.minAnimationAlpha().get();
                    }

                    leaf->_depth = info._lastAlpha;

                    if ( (!isText && !isBbox) || !fullyOut )
                    {
                        if ( info._lastAlpha > 0.01f && info._lastScale >= 0.0f )
                        {
                            leaves.push_back( leaf );

                            // scale it:
                            if ( info._lastScale != 1.0f )
                                leaf->_modelview->preMult( osg::Matrix::scale(info._lastScale,info._lastScale,1) );
                        }
                    }

                    info._frame++;
                    info._visible = false;
                }
            }
        }
    };


    /**
    * Custom draw routine for our declutter render bin.
    */
    struct DeclutterDraw : public osgUtil::RenderBin::DrawCallback
    {
        ScreenSpaceLayoutContext*                 _context;
        PerThread< osg::ref_ptr<osg::RefMatrix> > _ortho2D;
        osg::ref_ptr<osg::Uniform>                _fade;

        struct RunningState
        {
            RunningState() : lastFade(-1.0f), lastPCP(NULL) { }
            float lastFade;
            const osg::Program::PerContextProgram* lastPCP;
        };

        /**
        * Constructs the decluttering draw callback.
        * @param context A shared context among all decluttering objects.
        */
        DeclutterDraw( ScreenSpaceLayoutContext* context )
            : _context( context )
        {
            // create the fade uniform.
            _fade = new osg::Uniform( osg::Uniform::FLOAT, FADE_UNIFORM_NAME );
            _fade->set( 1.0f );
        }

        /**
        * Draws a bin. Most of this code is copied from osgUtil::RenderBin::drawImplementation.
        * The modifications are (a) skipping code to render child bins, (b) setting a bin-global
        * projection matrix in orthographic space, and (c) calling our custom "renderLeaf()" method
        * instead of RenderLeaf::render()
        */
        void drawImplementation( osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous )
        {
            osg::State& state = *renderInfo.getState();

            unsigned int numToPop = (previous ? osgUtil::StateGraph::numToPop(previous->_parent) : 0);
            if (numToPop>1) --numToPop;
            unsigned int insertStateSetPosition = state.getStateSetStackSize() - numToPop;

            if (bin->getStateSet())
            {
                state.insertStateSet(insertStateSetPosition, bin->getStateSet());
            }

            // apply a window-space projection matrix.
            const osg::Viewport* vp = renderInfo.getCurrentCamera()->getViewport();
            if ( vp )
            {
                osg::ref_ptr<osg::RefMatrix>& m = _ortho2D.get();
                if ( !m.valid() )
                    m = new osg::RefMatrix();

                //m->makeOrtho2D( vp->x(), vp->x()+vp->width()-1, vp->y(), vp->y()+vp->height()-1 );
                m->makeOrtho( vp->x(), vp->x()+vp->width()-1, vp->y(), vp->y()+vp->height()-1, -1000, 1000);
                state.applyProjectionMatrix( m.get() );
            }

            // initialize the fading uniform
            RunningState rs;

            // render the list
            osgUtil::RenderBin::RenderLeafList& leaves = bin->getRenderLeafList();

            for(osgUtil::RenderBin::RenderLeafList::reverse_iterator rlitr = leaves.rbegin();
                rlitr!= leaves.rend();
                ++rlitr)
            {
                osgUtil::RenderLeaf* rl = *rlitr;
                if ( rl->_depth > 0.0f)
                {
                    renderLeaf( rl, renderInfo, previous, rs);
                    previous = rl;
                }
            }

            if ( bin->getStateSet() )
            {
                state.removeStateSet(insertStateSetPosition);
            }
        }

        /**
        * Renders a single leaf. We already applied the projection matrix, so here we only
        * need to apply a modelview matrix that specifies the ortho offset of the drawable.
        *
        * Most of this code is copied from RenderLeaf::draw() -- but I removed all the code
        * dealing with nested bins, since decluttering does not support them.
        */
        void renderLeaf( osgUtil::RenderLeaf* leaf, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous, RunningState& rs)
        {
            osg::State& state = *renderInfo.getState();

            // don't draw this leaf if the abort rendering flag has been set.
            if (state.getAbortRendering())
            {
                //cout << "early abort"<<endl;
                return;
            }

            state.applyModelViewMatrix( leaf->_modelview.get() );

            if (previous)
            {
                // apply state if required.
                osgUtil::StateGraph* prev_rg = previous->_parent;
                osgUtil::StateGraph* prev_rg_parent = prev_rg->_parent;
                osgUtil::StateGraph* rg = leaf->_parent;
                if (prev_rg_parent!=rg->_parent)
                {
                    osgUtil::StateGraph::moveStateGraph(state,prev_rg_parent,rg->_parent);

                    // send state changes and matrix changes to OpenGL.
                    state.apply(rg->getStateSet());

                }
                else if (rg!=prev_rg)
                {
                    // send state changes and matrix changes to OpenGL.
                    state.apply(rg->getStateSet());
                }
            }
            else
            {
                // apply state if required.
                osgUtil::StateGraph::moveStateGraph(state,NULL,leaf->_parent->_parent);

                state.apply(leaf->_parent->getStateSet());
            }

            // if we are using osg::Program which requires OSG's generated uniforms to track
            // modelview and projection matrices then apply them now.
            if (state.getUseModelViewAndProjectionUniforms())
                state.applyModelViewAndProjectionUniformsIfRequired();

            // apply the fading uniform
            const osg::Program::PerContextProgram* pcp = state.getLastAppliedProgramObject();
            if ( pcp )
            {
                if (pcp != rs.lastPCP || leaf->_depth != rs.lastFade)
                {
                    rs.lastFade = ScreenSpaceLayout::globallyEnabled ? leaf->_depth : 1.0f;
                    _fade->set( rs.lastFade );
                    pcp->apply( *_fade.get() );
                }
            }
            rs.lastPCP = pcp;

            // draw the drawable
            leaf->_drawable->draw(renderInfo);

            if (leaf->_dynamic)
            {
                state.decrementDynamicObjectCount();
            }
        }
    };

} }
