2005-04-15 05:41:28 +08:00
|
|
|
/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2005 Robert Osfield
|
2003-01-22 00:45:36 +08:00
|
|
|
*
|
|
|
|
* This library is open source and may be redistributed and/or modified under
|
|
|
|
* the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or
|
|
|
|
* (at your option) any later version. The full license is in LICENSE file
|
|
|
|
* included with this distribution, and on the openscenegraph.org website.
|
|
|
|
*
|
|
|
|
* This library is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* OpenSceneGraph Public License for more details.
|
|
|
|
*/
|
2001-10-04 23:12:57 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
#ifndef OSG_POLYTOPE
|
|
|
|
#define OSG_POLYTOPE 1
|
2001-09-20 05:19:47 +08:00
|
|
|
|
|
|
|
#include <osg/Plane>
|
2002-06-03 23:39:41 +08:00
|
|
|
#include <osg/fast_back_stack>
|
2001-09-20 05:19:47 +08:00
|
|
|
|
|
|
|
namespace osg {
|
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
|
2004-09-03 03:10:33 +08:00
|
|
|
/** A Polytope class for representing convex clipping volumes made up of a set of planes.
|
2002-04-23 05:18:15 +08:00
|
|
|
* When adding planes, their normals should point inwards (into the volume) */
|
2005-04-12 01:14:17 +08:00
|
|
|
class OSG_EXPORT Polytope
|
2001-09-20 05:19:47 +08:00
|
|
|
{
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
2002-06-19 06:35:48 +08:00
|
|
|
typedef unsigned int ClippingMask;
|
|
|
|
typedef std::vector<Plane> PlaneList;
|
|
|
|
typedef std::vector<Vec3> VertexList;
|
|
|
|
typedef fast_back_stack<ClippingMask> MaskStack;
|
2001-09-20 05:19:47 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline Polytope() {setupMask();}
|
2002-05-18 16:39:42 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline Polytope(const Polytope& cv) :
|
|
|
|
_maskStack(cv._maskStack),
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask(cv._resultMask),
|
2002-06-19 06:35:48 +08:00
|
|
|
_planeList(cv._planeList),
|
|
|
|
_referenceVertexList(cv._referenceVertexList) {}
|
2001-09-20 05:19:47 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline Polytope(const PlaneList& pl) : _planeList(pl) {setupMask();}
|
2001-09-20 05:19:47 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline ~Polytope() {}
|
2001-09-20 05:19:47 +08:00
|
|
|
|
|
|
|
inline void clear() { _planeList.clear(); setupMask(); }
|
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline Polytope& operator = (const Polytope& cv)
|
2001-09-20 05:19:47 +08:00
|
|
|
{
|
|
|
|
if (&cv==this) return *this;
|
2002-06-03 23:39:41 +08:00
|
|
|
_maskStack = cv._maskStack;
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = cv._resultMask;
|
2002-06-03 23:39:41 +08:00
|
|
|
_planeList = cv._planeList;
|
2002-06-19 06:35:48 +08:00
|
|
|
_referenceVertexList = cv._referenceVertexList;
|
2002-06-03 23:39:41 +08:00
|
|
|
return *this;
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
2002-04-01 00:40:44 +08:00
|
|
|
|
2004-09-03 03:10:33 +08:00
|
|
|
/** Create a Polytope which is a cube, centered at 0,0,0, with sides of 2 units.*/
|
2002-06-15 20:14:42 +08:00
|
|
|
void setToUnitFrustum(bool withNear=true, bool withFar=true)
|
2002-04-01 00:40:44 +08:00
|
|
|
{
|
2002-05-23 23:35:12 +08:00
|
|
|
_planeList.erase(_planeList.begin(),_planeList.end());
|
2002-04-01 00:40:44 +08:00
|
|
|
_planeList.push_back(Plane(1.0f,0.0f,0.0f,1.0f)); // left plane.
|
|
|
|
_planeList.push_back(Plane(-1.0f,0.0f,0.0f,1.0f)); // right plane.
|
|
|
|
_planeList.push_back(Plane(0.0f,1.0f,0.0f,1.0f)); // bottom plane.
|
|
|
|
_planeList.push_back(Plane(0.0f,-1.0f,0.0f,1.0f)); // top plane.
|
2002-06-15 20:14:42 +08:00
|
|
|
if (withNear) _planeList.push_back(Plane(0.0f,0.0f,-1.0f,1.0f)); // near plane
|
|
|
|
if (withFar) _planeList.push_back(Plane(0.0f,0.0f,1.0f,1.0f)); // far plane
|
2002-04-01 00:40:44 +08:00
|
|
|
setupMask();
|
|
|
|
}
|
2001-09-20 05:19:47 +08:00
|
|
|
|
2003-12-04 05:45:32 +08:00
|
|
|
inline void setAndTransformProvidingInverse(const Polytope& pt, const osg::Matrix& matrix)
|
|
|
|
{
|
|
|
|
_referenceVertexList = pt._referenceVertexList;
|
|
|
|
|
|
|
|
unsigned int resultMask = pt._maskStack.back();
|
|
|
|
if (resultMask==0)
|
|
|
|
{
|
|
|
|
_maskStack.back() = 0;
|
|
|
|
_resultMask = 0;
|
|
|
|
_planeList.clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ClippingMask selector_mask = 0x1;
|
|
|
|
|
|
|
|
unsigned int numActivePlanes = 0;
|
|
|
|
|
2004-09-03 03:10:33 +08:00
|
|
|
// count number of active planes.
|
2003-12-04 05:45:32 +08:00
|
|
|
PlaneList::const_iterator itr;
|
|
|
|
for(itr=pt._planeList.begin();
|
|
|
|
itr!=pt._planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
if (resultMask&selector_mask) ++numActivePlanes;
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
_planeList.resize(numActivePlanes);
|
|
|
|
_resultMask = 0;
|
|
|
|
selector_mask = 0x1;
|
|
|
|
unsigned int index = 0;
|
|
|
|
for(itr=pt._planeList.begin();
|
|
|
|
itr!=pt._planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
if (resultMask&selector_mask)
|
|
|
|
{
|
|
|
|
_planeList[index] = *itr;
|
|
|
|
_planeList[index++].transformProvidingInverse(matrix);
|
|
|
|
_resultMask = (_resultMask<<1) | 1;
|
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
_maskStack.back() = _resultMask;
|
|
|
|
}
|
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
inline void set(const PlaneList& pl) { _planeList = pl; setupMask(); }
|
|
|
|
|
2003-12-04 05:45:32 +08:00
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
inline void add(const osg::Plane& pl) { _planeList.push_back(pl); setupMask(); }
|
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
/** flip/reverse the orientation of all the planes.*/
|
|
|
|
inline void flip()
|
|
|
|
{
|
|
|
|
for(PlaneList::iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
itr->flip();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
inline PlaneList& getPlaneList() { return _planeList; }
|
|
|
|
|
|
|
|
inline const PlaneList& getPlaneList() const { return _planeList; }
|
|
|
|
|
2002-06-19 06:35:48 +08:00
|
|
|
|
|
|
|
inline void setReferenceVertexList(VertexList& vertices) { _referenceVertexList=vertices; }
|
|
|
|
|
|
|
|
inline VertexList& getReferenceVertexList() { return _referenceVertexList; }
|
|
|
|
|
|
|
|
inline const VertexList& getReferenceVertexList() const { return _referenceVertexList; }
|
|
|
|
|
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
inline void setupMask()
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = 0;
|
2001-09-20 05:19:47 +08:00
|
|
|
for(unsigned int i=0;i<_planeList.size();++i)
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = (_resultMask<<1) | 1;
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
2002-06-09 03:58:05 +08:00
|
|
|
_maskStack.back() = _resultMask;
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline ClippingMask& getCurrentMask() { return _maskStack.back(); }
|
2001-09-20 05:19:47 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
inline ClippingMask getCurrentMask() const { return _maskStack.back(); }
|
|
|
|
|
2002-06-09 03:58:05 +08:00
|
|
|
inline void setResultMask(ClippingMask mask) { _resultMask=mask; }
|
|
|
|
|
|
|
|
inline ClippingMask getResultMask() const { return _resultMask; }
|
|
|
|
|
2002-06-17 17:10:26 +08:00
|
|
|
MaskStack& getMaskStack() { return _maskStack; }
|
|
|
|
|
|
|
|
const MaskStack& getMaskStack() const { return _maskStack; }
|
2002-06-09 03:58:05 +08:00
|
|
|
|
2002-06-03 23:39:41 +08:00
|
|
|
|
|
|
|
inline void pushCurrentMask()
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
_maskStack.push_back(_resultMask);
|
2002-06-03 23:39:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
inline void popCurrentMask()
|
|
|
|
{
|
|
|
|
_maskStack.pop_back();
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
|
|
|
|
2004-09-03 03:10:33 +08:00
|
|
|
/** Check whether a vertex is contained within clipping set.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool contains(const osg::Vec3& v) const
|
2002-05-18 16:39:42 +08:00
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if (!_maskStack.back()) return true;
|
|
|
|
|
2002-05-18 16:39:42 +08:00
|
|
|
unsigned int selector_mask = 0x1;
|
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if ((_maskStack.back()&selector_mask) && (itr->distance(v)<0.0f)) return false;
|
2002-05-18 16:39:42 +08:00
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2004-09-03 03:10:33 +08:00
|
|
|
/** Check whether any part of vertex list is contained within clipping set.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool contains(const std::vector<Vec3>& vertices)
|
2002-06-12 17:22:30 +08:00
|
|
|
{
|
|
|
|
if (!_maskStack.back()) return true;
|
|
|
|
|
|
|
|
_resultMask = _maskStack.back();
|
|
|
|
ClippingMask selector_mask = 0x1;
|
|
|
|
|
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
if (_resultMask&selector_mask)
|
|
|
|
{
|
|
|
|
int res=itr->intersect(vertices);
|
|
|
|
if (res<0) return false; // outside clipping set.
|
|
|
|
else if (res>0) _resultMask ^= selector_mask; // subsequent checks against this plane not required.
|
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
/** Check whether any part of a bounding sphere is contained within clipping set.
|
|
|
|
Using a mask to determine which planes should be used for the check, and
|
|
|
|
modifying the mask to turn off planes which wouldn't contribute to clipping
|
|
|
|
of any internal objects. This feature is used in osgUtil::CullVisitor
|
2001-10-01 19:15:55 +08:00
|
|
|
to prevent redundant plane checking.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool contains(const osg::BoundingSphere& bs)
|
2001-09-20 05:19:47 +08:00
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if (!_maskStack.back()) return true;
|
|
|
|
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = _maskStack.back();
|
2002-06-03 23:39:41 +08:00
|
|
|
ClippingMask selector_mask = 0x1;
|
2001-09-20 05:19:47 +08:00
|
|
|
|
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
if (_resultMask&selector_mask)
|
2001-09-20 05:19:47 +08:00
|
|
|
{
|
|
|
|
int res=itr->intersect(bs);
|
|
|
|
if (res<0) return false; // outside clipping set.
|
2002-06-09 03:58:05 +08:00
|
|
|
else if (res>0) _resultMask ^= selector_mask; // subsequent checks against this plane not required.
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Check whether any part of a bounding box is contained within clipping set.
|
|
|
|
Using a mask to determine which planes should be used for the check, and
|
|
|
|
modifying the mask to turn off planes which wouldn't contribute to clipping
|
|
|
|
of any internal objects. This feature is used in osgUtil::CullVisitor
|
2001-10-01 19:15:55 +08:00
|
|
|
to prevent redundant plane checking.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool contains(const osg::BoundingBox& bb)
|
2001-09-20 05:19:47 +08:00
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if (!_maskStack.back()) return true;
|
|
|
|
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = _maskStack.back();
|
2002-06-03 23:39:41 +08:00
|
|
|
ClippingMask selector_mask = 0x1;
|
2001-09-20 05:19:47 +08:00
|
|
|
|
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
if (_resultMask&selector_mask)
|
2001-09-20 05:19:47 +08:00
|
|
|
{
|
|
|
|
int res=itr->intersect(bb);
|
|
|
|
if (res<0) return false; // outside clipping set.
|
2002-06-09 03:58:05 +08:00
|
|
|
else if (res>0) _resultMask ^= selector_mask; // subsequent checks against this plane not required.
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2002-06-12 17:22:30 +08:00
|
|
|
/** Check whether all of vertex list is contained with clipping set.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool containsAllOf(const std::vector<Vec3>& vertices)
|
2002-06-12 17:22:30 +08:00
|
|
|
{
|
|
|
|
if (!_maskStack.back()) return false;
|
|
|
|
|
|
|
|
_resultMask = _maskStack.back();
|
|
|
|
ClippingMask selector_mask = 0x1;
|
|
|
|
|
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
if (_resultMask&selector_mask)
|
|
|
|
{
|
|
|
|
int res=itr->intersect(vertices);
|
|
|
|
if (res<1) return false; // intersects, or is below plane.
|
|
|
|
_resultMask ^= selector_mask; // subsequent checks against this plane not required.
|
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2002-04-23 05:18:15 +08:00
|
|
|
/** Check whether the entire bounding sphere is contained within clipping set.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool containsAllOf(const osg::BoundingSphere& bs)
|
2002-04-23 05:18:15 +08:00
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if (!_maskStack.back()) return false;
|
|
|
|
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = _maskStack.back();
|
2002-06-03 23:39:41 +08:00
|
|
|
ClippingMask selector_mask = 0x1;
|
|
|
|
|
2002-04-23 05:18:15 +08:00
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
if (_resultMask&selector_mask)
|
2002-06-03 23:39:41 +08:00
|
|
|
{
|
|
|
|
int res=itr->intersect(bs);
|
|
|
|
if (res<1) return false; // intersects, or is below plane.
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask ^= selector_mask; // subsequent checks against this plane not required.
|
2002-06-03 23:39:41 +08:00
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
2002-04-23 05:18:15 +08:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Check whether the entire bounding box is contained within clipping set.*/
|
2002-09-02 20:31:35 +08:00
|
|
|
inline bool containsAllOf(const osg::BoundingBox& bb)
|
2002-04-23 05:18:15 +08:00
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if (!_maskStack.back()) return false;
|
|
|
|
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = _maskStack.back();
|
2002-06-03 23:39:41 +08:00
|
|
|
ClippingMask selector_mask = 0x1;
|
|
|
|
|
2002-04-23 05:18:15 +08:00
|
|
|
for(PlaneList::const_iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
if (_resultMask&selector_mask)
|
2002-06-03 23:39:41 +08:00
|
|
|
{
|
|
|
|
int res=itr->intersect(bb);
|
|
|
|
if (res<1) return false; // intersects, or is below plane.
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask ^= selector_mask; // subsequent checks against this plane not required.
|
2002-06-03 23:39:41 +08:00
|
|
|
}
|
|
|
|
selector_mask <<= 1;
|
2002-04-23 05:18:15 +08:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2001-09-20 05:19:47 +08:00
|
|
|
|
2002-04-23 05:18:15 +08:00
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
/** Transform the clipping set by matrix. Note, this operations carries out
|
2004-09-03 03:10:33 +08:00
|
|
|
* the calculation of the inverse of the matrix since a plane must
|
|
|
|
* be multiplied by the inverse transposed to transform it. This
|
|
|
|
* makes this operation expensive. If the inverse has been already
|
2001-09-20 05:19:47 +08:00
|
|
|
* calculated elsewhere then use transformProvidingInverse() instead.
|
|
|
|
* See http://www.worldserver.com/turk/computergraphics/NormalTransformations.pdf*/
|
|
|
|
inline void transform(const osg::Matrix& matrix)
|
|
|
|
{
|
|
|
|
osg::Matrix inverse;
|
|
|
|
inverse.invert(matrix);
|
|
|
|
transformProvidingInverse(inverse);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Transform the clipping set by provide a pre inverted matrix.
|
|
|
|
* see transform for details. */
|
|
|
|
inline void transformProvidingInverse(const osg::Matrix& matrix)
|
|
|
|
{
|
2002-06-03 23:39:41 +08:00
|
|
|
if (!_maskStack.back()) return;
|
|
|
|
|
2002-06-09 03:58:05 +08:00
|
|
|
_resultMask = _maskStack.back();
|
2002-06-03 23:39:41 +08:00
|
|
|
ClippingMask selector_mask = 0x1;
|
2001-09-20 05:19:47 +08:00
|
|
|
for(PlaneList::iterator itr=_planeList.begin();
|
|
|
|
itr!=_planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-09 03:58:05 +08:00
|
|
|
if (_resultMask&selector_mask)
|
2002-06-03 23:39:41 +08:00
|
|
|
{
|
|
|
|
itr->transformProvidingInverse(matrix);
|
|
|
|
selector_mask <<= 1;
|
|
|
|
}
|
2001-09-20 05:19:47 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
|
|
|
|
2002-06-17 17:10:26 +08:00
|
|
|
MaskStack _maskStack;
|
2002-06-09 03:58:05 +08:00
|
|
|
ClippingMask _resultMask;
|
2002-06-03 23:39:41 +08:00
|
|
|
PlaneList _planeList;
|
2002-06-19 06:35:48 +08:00
|
|
|
VertexList _referenceVertexList;
|
2001-09-20 05:19:47 +08:00
|
|
|
|
|
|
|
};
|
|
|
|
|
2005-11-18 01:44:48 +08:00
|
|
|
} // end of namespace
|
2002-02-03 20:33:41 +08:00
|
|
|
|
2001-09-20 05:19:47 +08:00
|
|
|
#endif
|