Initial prototype of knob animation.
This commit is contained in:
parent
a58b41e7d2
commit
4d4e474464
@ -27,11 +27,12 @@
|
||||
#include <osg/PolygonMode>
|
||||
#include <osg/Material>
|
||||
|
||||
#include <simgear/scene/material/EffectGeode.hxx>
|
||||
#include <simgear/scene/util/SGPickCallback.hxx>
|
||||
#include <simgear/scene/material/EffectGeode.hxx>
|
||||
#include <simgear/scene/util/SGSceneUserData.hxx>
|
||||
#include <simgear/structure/SGBinding.hxx>
|
||||
#include <simgear/scene/util/StateAttributeFactory.hxx>
|
||||
#include <simgear/scene/model/SGRotateTransform.hxx>
|
||||
|
||||
using namespace simgear;
|
||||
|
||||
@ -76,17 +77,14 @@ using OpenThreads::ScopedLock;
|
||||
}
|
||||
if (!found )
|
||||
return false;
|
||||
SGBindingList::const_iterator i;
|
||||
for (i = _bindingsDown.begin(); i != _bindingsDown.end(); ++i)
|
||||
(*i)->fire();
|
||||
|
||||
fireBindingList(_bindingsDown);
|
||||
_repeatTime = -_repeatInterval; // anti-bobble: delay start of repeat
|
||||
return true;
|
||||
}
|
||||
virtual void buttonReleased(void)
|
||||
{
|
||||
SGBindingList::const_iterator i;
|
||||
for (i = _bindingsUp.begin(); i != _bindingsUp.end(); ++i)
|
||||
(*i)->fire();
|
||||
fireBindingList(_bindingsUp);
|
||||
}
|
||||
virtual void update(double dt)
|
||||
{
|
||||
@ -96,9 +94,7 @@ using OpenThreads::ScopedLock;
|
||||
_repeatTime += dt;
|
||||
while (_repeatInterval < _repeatTime) {
|
||||
_repeatTime -= _repeatInterval;
|
||||
SGBindingList::const_iterator i;
|
||||
for (i = _bindingsDown.begin(); i != _bindingsDown.end(); ++i)
|
||||
(*i)->fire();
|
||||
fireBindingList(_bindingsDown);
|
||||
}
|
||||
}
|
||||
private:
|
||||
@ -252,23 +248,81 @@ using OpenThreads::ScopedLock;
|
||||
osg::ref_ptr<osg::Uniform> colorModeUniform;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SGPickAnimation::innerSetupPickGroup(osg::Group* commonGroup, osg::Group& parent)
|
||||
{
|
||||
// Contains the normal geometry that is interactive
|
||||
osg::ref_ptr<osg::Group> normalGroup = new osg::Group;
|
||||
normalGroup->setName("pick normal group");
|
||||
normalGroup->addChild(commonGroup);
|
||||
|
||||
// Used to render the geometry with just yellow edges
|
||||
osg::Group* highlightGroup = new osg::Group;
|
||||
highlightGroup->setName("pick highlight group");
|
||||
highlightGroup->setNodeMask(simgear::PICK_BIT);
|
||||
highlightGroup->addChild(commonGroup);
|
||||
|
||||
// prepare a state set that paints the edges of this object yellow
|
||||
// The material and texture attributes are set with
|
||||
// OVERRIDE|PROTECTED in case there is a material animation on a
|
||||
// higher node in the scene graph, which would have its material
|
||||
// attribute set with OVERRIDE.
|
||||
osg::StateSet* stateSet = highlightGroup->getOrCreateStateSet();
|
||||
osg::Texture2D* white = StateAttributeFactory::instance()->getWhiteTexture();
|
||||
stateSet->setTextureAttributeAndModes(0, white,
|
||||
(osg::StateAttribute::ON
|
||||
| osg::StateAttribute::OVERRIDE
|
||||
| osg::StateAttribute::PROTECTED));
|
||||
osg::PolygonOffset* polygonOffset = new osg::PolygonOffset;
|
||||
polygonOffset->setFactor(-1);
|
||||
polygonOffset->setUnits(-1);
|
||||
stateSet->setAttribute(polygonOffset, osg::StateAttribute::OVERRIDE);
|
||||
stateSet->setMode(GL_POLYGON_OFFSET_LINE,
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
osg::PolygonMode* polygonMode = new osg::PolygonMode;
|
||||
polygonMode->setMode(osg::PolygonMode::FRONT_AND_BACK,
|
||||
osg::PolygonMode::LINE);
|
||||
stateSet->setAttribute(polygonMode, osg::StateAttribute::OVERRIDE);
|
||||
osg::Material* material = new osg::Material;
|
||||
material->setColorMode(osg::Material::OFF);
|
||||
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1));
|
||||
// XXX Alpha < 1.0 in the diffuse material value is a signal to the
|
||||
// default shader to take the alpha value from the material value
|
||||
// and not the glColor. In many cases the pick animation geometry is
|
||||
// transparent, so the outline would not be visible without this hack.
|
||||
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, .95));
|
||||
material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 0, 1));
|
||||
material->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 0));
|
||||
stateSet->setAttribute(
|
||||
material, osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED);
|
||||
// The default shader has a colorMode uniform that mimics the
|
||||
// behavior of Material color mode.
|
||||
osg::Uniform* cmUniform = 0;
|
||||
{
|
||||
ScopedLock<Mutex> lock(colorModeUniformMutex);
|
||||
if (!colorModeUniform.valid()) {
|
||||
colorModeUniform = new osg::Uniform(osg::Uniform::INT, "colorMode");
|
||||
colorModeUniform->set(0); // MODE_OFF
|
||||
colorModeUniform->setDataVariance(osg::Object::STATIC);
|
||||
}
|
||||
cmUniform = colorModeUniform.get();
|
||||
}
|
||||
stateSet->addUniform(cmUniform,
|
||||
osg::StateAttribute::OVERRIDE | osg::StateAttribute::ON);
|
||||
|
||||
// Only add normal geometry if configured
|
||||
if (getConfig()->getBoolValue("visible", true))
|
||||
parent.addChild(normalGroup.get());
|
||||
parent.addChild(highlightGroup);
|
||||
}
|
||||
|
||||
osg::Group*
|
||||
SGPickAnimation::createAnimationGroup(osg::Group& parent)
|
||||
{
|
||||
osg::Group* commonGroup = new osg::Group;
|
||||
|
||||
// Contains the normal geometry that is interactive
|
||||
osg::ref_ptr<osg::Group> normalGroup = new osg::Group;
|
||||
normalGroup->setName("pick normal group");
|
||||
normalGroup->addChild(commonGroup);
|
||||
|
||||
// Used to render the geometry with just yellow edges
|
||||
osg::Group* highlightGroup = new osg::Group;
|
||||
highlightGroup->setName("pick highlight group");
|
||||
highlightGroup->setNodeMask(simgear::PICK_BIT);
|
||||
highlightGroup->addChild(commonGroup);
|
||||
SGSceneUserData* ud;
|
||||
ud = SGSceneUserData::getOrCreateSceneUserData(commonGroup);
|
||||
osg::Group* commonGroup = new osg::Group;
|
||||
innerSetupPickGroup(commonGroup, parent);
|
||||
SGSceneUserData* ud = SGSceneUserData::getOrCreateSceneUserData(commonGroup);
|
||||
|
||||
// add actions that become macro and command invocations
|
||||
std::vector<SGPropertyNode_ptr> actions;
|
||||
@ -281,59 +335,137 @@ using OpenThreads::ScopedLock;
|
||||
ud->addPickCallback(new VncCallback(actions[i], getModelRoot(),
|
||||
&parent));
|
||||
|
||||
// prepare a state set that paints the edges of this object yellow
|
||||
// The material and texture attributes are set with
|
||||
// OVERRIDE|PROTECTED in case there is a material animation on a
|
||||
// higher node in the scene graph, which would have its material
|
||||
// attribute set with OVERRIDE.
|
||||
osg::StateSet* stateSet = highlightGroup->getOrCreateStateSet();
|
||||
osg::Texture2D* white = StateAttributeFactory::instance()->getWhiteTexture();
|
||||
stateSet->setTextureAttributeAndModes(0, white,
|
||||
(osg::StateAttribute::ON
|
||||
| osg::StateAttribute::OVERRIDE
|
||||
| osg::StateAttribute::PROTECTED));
|
||||
osg::PolygonOffset* polygonOffset = new osg::PolygonOffset;
|
||||
polygonOffset->setFactor(-1);
|
||||
polygonOffset->setUnits(-1);
|
||||
stateSet->setAttribute(polygonOffset, osg::StateAttribute::OVERRIDE);
|
||||
stateSet->setMode(GL_POLYGON_OFFSET_LINE,
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
osg::PolygonMode* polygonMode = new osg::PolygonMode;
|
||||
polygonMode->setMode(osg::PolygonMode::FRONT_AND_BACK,
|
||||
osg::PolygonMode::LINE);
|
||||
stateSet->setAttribute(polygonMode, osg::StateAttribute::OVERRIDE);
|
||||
osg::Material* material = new osg::Material;
|
||||
material->setColorMode(osg::Material::OFF);
|
||||
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1));
|
||||
// XXX Alpha < 1.0 in the diffuse material value is a signal to the
|
||||
// default shader to take the alpha value from the material value
|
||||
// and not the glColor. In many cases the pick animation geometry is
|
||||
// transparent, so the outline would not be visible without this hack.
|
||||
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, .95));
|
||||
material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 0, 1));
|
||||
material->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 0));
|
||||
stateSet->setAttribute(
|
||||
material, osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED);
|
||||
// The default shader has a colorMode uniform that mimics the
|
||||
// behavior of Material color mode.
|
||||
osg::Uniform* cmUniform = 0;
|
||||
{
|
||||
ScopedLock<Mutex> lock(colorModeUniformMutex);
|
||||
if (!colorModeUniform.valid()) {
|
||||
colorModeUniform = new osg::Uniform(osg::Uniform::INT, "colorMode");
|
||||
colorModeUniform->set(0); // MODE_OFF
|
||||
colorModeUniform->setDataVariance(osg::Object::STATIC);
|
||||
}
|
||||
cmUniform = colorModeUniform.get();
|
||||
}
|
||||
stateSet->addUniform(cmUniform,
|
||||
osg::StateAttribute::OVERRIDE | osg::StateAttribute::ON);
|
||||
// Only add normal geometry if configured
|
||||
if (getConfig()->getBoolValue("visible", true))
|
||||
parent.addChild(normalGroup.get());
|
||||
parent.addChild(highlightGroup);
|
||||
|
||||
|
||||
return commonGroup;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class SGKnobAnimation::KnobPickCallback : public SGPickCallback {
|
||||
public:
|
||||
KnobPickCallback(const SGPropertyNode* configNode,
|
||||
SGPropertyNode* modelRoot) :
|
||||
_direction(DIRECTION_NONE),
|
||||
_repeatInterval(configNode->getDoubleValue("interval-sec", 0.1))
|
||||
{
|
||||
const SGPropertyNode* act = configNode->getChild("action");
|
||||
if (act)
|
||||
_action = readBindingList(act->getChildren("binding"), modelRoot);
|
||||
|
||||
const SGPropertyNode* cw = configNode->getChild("cw");
|
||||
if (cw)
|
||||
_bindingsCW = readBindingList(cw->getChildren("binding"), modelRoot);
|
||||
|
||||
const SGPropertyNode* ccw = configNode->getChild("ccw");
|
||||
if (ccw)
|
||||
_bindingsCCW = readBindingList(ccw->getChildren("binding"), modelRoot);
|
||||
}
|
||||
|
||||
virtual bool buttonPressed(int button, const Info&)
|
||||
{
|
||||
_direction = DIRECTION_NONE;
|
||||
if ((button == 0) || (button == 4)) {
|
||||
_direction = DIRECTION_CLOCKWISE;
|
||||
} else if ((button == 1) || (button == 3)) {
|
||||
_direction = DIRECTION_ANTICLOCKWISE;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
_repeatTime = -_repeatInterval; // anti-bobble: delay start of repeat
|
||||
fire();
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void buttonReleased(void)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void update(double dt)
|
||||
{
|
||||
_repeatTime += dt;
|
||||
while (_repeatInterval < _repeatTime) {
|
||||
_repeatTime -= _repeatInterval;
|
||||
fire();
|
||||
} // of repeat iteration
|
||||
}
|
||||
private:
|
||||
void fire()
|
||||
{
|
||||
switch (_direction) {
|
||||
case DIRECTION_CLOCKWISE:
|
||||
fireBindingListWithOffset(_action, 1, 1);
|
||||
fireBindingList(_bindingsCW);
|
||||
break;
|
||||
case DIRECTION_ANTICLOCKWISE:
|
||||
fireBindingListWithOffset(_action, -1, 1);
|
||||
fireBindingList(_bindingsCCW);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
SGBindingList _action;
|
||||
SGBindingList _bindingsCW,
|
||||
_bindingsCCW;
|
||||
|
||||
enum Direction
|
||||
{
|
||||
DIRECTION_NONE,
|
||||
DIRECTION_CLOCKWISE,
|
||||
DIRECTION_ANTICLOCKWISE
|
||||
};
|
||||
|
||||
Direction _direction;
|
||||
double _repeatInterval;
|
||||
double _repeatTime;
|
||||
};
|
||||
|
||||
class SGKnobAnimation::UpdateCallback : public osg::NodeCallback {
|
||||
public:
|
||||
UpdateCallback(SGExpressiond const* animationValue) :
|
||||
_animationValue(animationValue)
|
||||
{
|
||||
setName("SGKnobAnimation::UpdateCallback");
|
||||
}
|
||||
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
|
||||
{
|
||||
SGRotateTransform* transform = static_cast<SGRotateTransform*>(node);
|
||||
transform->setAngleDeg(_animationValue->getValue());
|
||||
traverse(node, nv);
|
||||
}
|
||||
|
||||
private:
|
||||
SGSharedPtr<SGExpressiond const> _animationValue;
|
||||
};
|
||||
|
||||
|
||||
SGKnobAnimation::SGKnobAnimation(const SGPropertyNode* configNode,
|
||||
SGPropertyNode* modelRoot) :
|
||||
SGPickAnimation(configNode, modelRoot)
|
||||
{
|
||||
SGSharedPtr<SGExpressiond> value = read_value(configNode, modelRoot, "-deg",
|
||||
-SGLimitsd::max(), SGLimitsd::max());
|
||||
_animationValue = value->simplify();
|
||||
|
||||
|
||||
readRotationCenterAndAxis(configNode, _center, _axis);
|
||||
}
|
||||
|
||||
|
||||
osg::Group*
|
||||
SGKnobAnimation::createAnimationGroup(osg::Group& parent)
|
||||
{
|
||||
SGRotateTransform* transform = new SGRotateTransform();
|
||||
innerSetupPickGroup(transform, parent);
|
||||
|
||||
UpdateCallback* uc = new UpdateCallback(_animationValue);
|
||||
transform->setUpdateCallback(uc);
|
||||
transform->setCenter(_center);
|
||||
transform->setAxis(_axis);
|
||||
|
||||
SGSceneUserData* ud = SGSceneUserData::getOrCreateSceneUserData(transform);
|
||||
ud->setPickCallback(new KnobPickCallback(getConfig(), getModelRoot()));
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,8 @@
|
||||
|
||||
#include <simgear/scene/model/animation.hxx>
|
||||
|
||||
// forward decls
|
||||
class SGPickCallback;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Pick animation
|
||||
@ -34,10 +36,32 @@ public:
|
||||
SGPickAnimation(const SGPropertyNode* configNode,
|
||||
SGPropertyNode* modelRoot);
|
||||
virtual osg::Group* createAnimationGroup(osg::Group& parent);
|
||||
|
||||
|
||||
protected:
|
||||
void innerSetupPickGroup(osg::Group* commonGroup, osg::Group& parent);
|
||||
|
||||
private:
|
||||
class PickCallback;
|
||||
class VncCallback;
|
||||
};
|
||||
|
||||
|
||||
class SGKnobAnimation : public SGPickAnimation
|
||||
{
|
||||
public:
|
||||
SGKnobAnimation(const SGPropertyNode* configNode,
|
||||
SGPropertyNode* modelRoot);
|
||||
virtual osg::Group* createAnimationGroup(osg::Group& parent);
|
||||
|
||||
private:
|
||||
class KnobPickCallback;
|
||||
class UpdateCallback;
|
||||
|
||||
SGVec3d _axis;
|
||||
SGVec3d _center;
|
||||
SGSharedPtr<SGExpressiond const> _animationValue;
|
||||
};
|
||||
|
||||
#endif // of SG_SCENE_PICK_ANIMATION_HXX
|
||||
|
||||
|
@ -396,6 +396,9 @@ SGAnimation::animate(osg::Node* node, const SGPropertyNode* configNode,
|
||||
} else if (type == "pick") {
|
||||
SGPickAnimation animInst(configNode, modelRoot);
|
||||
animInst.apply(node);
|
||||
} else if (type == "knob") {
|
||||
SGKnobAnimation animInst(configNode, modelRoot);
|
||||
animInst.apply(node);
|
||||
} else if (type == "range") {
|
||||
SGRangeAnimation animInst(configNode, modelRoot);
|
||||
animInst.apply(node);
|
||||
@ -846,6 +849,33 @@ void SpinAnimCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
|
||||
}
|
||||
}
|
||||
|
||||
// factored out to share with SGKnobAnimation
|
||||
void readRotationCenterAndAxis(const SGPropertyNode* configNode, SGVec3d& center, SGVec3d& axis)
|
||||
{
|
||||
center = SGVec3d::zeros();
|
||||
if (configNode->hasValue("axis/x1-m")) {
|
||||
SGVec3d v1, v2;
|
||||
v1[0] = configNode->getDoubleValue("axis/x1-m", 0);
|
||||
v1[1] = configNode->getDoubleValue("axis/y1-m", 0);
|
||||
v1[2] = configNode->getDoubleValue("axis/z1-m", 0);
|
||||
v2[0] = configNode->getDoubleValue("axis/x2-m", 0);
|
||||
v2[1] = configNode->getDoubleValue("axis/y2-m", 0);
|
||||
v2[2] = configNode->getDoubleValue("axis/z2-m", 0);
|
||||
center = 0.5*(v1+v2);
|
||||
axis = v2 - v1;
|
||||
} else {
|
||||
axis[0] = configNode->getDoubleValue("axis/x", 0);
|
||||
axis[1] = configNode->getDoubleValue("axis/y", 0);
|
||||
axis[2] = configNode->getDoubleValue("axis/z", 0);
|
||||
}
|
||||
if (8*SGLimitsd::min() < norm(axis))
|
||||
axis = normalize(axis);
|
||||
|
||||
center[0] = configNode->getDoubleValue("center/x-m", center[0]);
|
||||
center[1] = configNode->getDoubleValue("center/y-m", center[1]);
|
||||
center[2] = configNode->getDoubleValue("center/z-m", center[2]);
|
||||
}
|
||||
|
||||
SGRotateAnimation::SGRotateAnimation(const SGPropertyNode* configNode,
|
||||
SGPropertyNode* modelRoot) :
|
||||
SGAnimation(configNode, modelRoot)
|
||||
@ -862,28 +892,8 @@ SGRotateAnimation::SGRotateAnimation(const SGPropertyNode* configNode,
|
||||
_initialValue = _animationValue->getValue();
|
||||
else
|
||||
_initialValue = 0;
|
||||
_center = SGVec3d::zeros();
|
||||
if (configNode->hasValue("axis/x1-m")) {
|
||||
SGVec3d v1, v2;
|
||||
v1[0] = configNode->getDoubleValue("axis/x1-m", 0);
|
||||
v1[1] = configNode->getDoubleValue("axis/y1-m", 0);
|
||||
v1[2] = configNode->getDoubleValue("axis/z1-m", 0);
|
||||
v2[0] = configNode->getDoubleValue("axis/x2-m", 0);
|
||||
v2[1] = configNode->getDoubleValue("axis/y2-m", 0);
|
||||
v2[2] = configNode->getDoubleValue("axis/z2-m", 0);
|
||||
_center = 0.5*(v1+v2);
|
||||
_axis = v2 - v1;
|
||||
} else {
|
||||
_axis[0] = configNode->getDoubleValue("axis/x", 0);
|
||||
_axis[1] = configNode->getDoubleValue("axis/y", 0);
|
||||
_axis[2] = configNode->getDoubleValue("axis/z", 0);
|
||||
}
|
||||
if (8*SGLimitsd::min() < norm(_axis))
|
||||
_axis = normalize(_axis);
|
||||
|
||||
_center[0] = configNode->getDoubleValue("center/x-m", _center[0]);
|
||||
_center[1] = configNode->getDoubleValue("center/y-m", _center[1]);
|
||||
_center[2] = configNode->getDoubleValue("center/z-m", _center[2]);
|
||||
|
||||
readRotationCenterAndAxis(configNode, _center, _axis);
|
||||
}
|
||||
|
||||
osg::Group*
|
||||
|
@ -34,7 +34,9 @@
|
||||
SGExpressiond*
|
||||
read_value(const SGPropertyNode* configNode, SGPropertyNode* modelRoot,
|
||||
const char* unit, double defMin, double defMax);
|
||||
|
||||
|
||||
void readRotationCenterAndAxis(const SGPropertyNode* configNode, SGVec3d& center, SGVec3d& axis);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Base class for animation installers
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
@ -114,3 +114,20 @@ void fireBindingList(const SGBindingList& aBindings)
|
||||
b->fire();
|
||||
}
|
||||
}
|
||||
|
||||
void fireBindingListWithOffset(const SGBindingList& aBindings, double offset, double max)
|
||||
{
|
||||
BOOST_FOREACH(SGBinding_ptr b, aBindings) {
|
||||
b->fire(offset, max);
|
||||
}
|
||||
}
|
||||
|
||||
SGBindingList readBindingList(const simgear::PropertyList& aNodes, SGPropertyNode* aRoot)
|
||||
{
|
||||
SGBindingList result;
|
||||
BOOST_FOREACH(SGPropertyNode* node, aNodes) {
|
||||
result.push_back(new SGBinding(node, aRoot));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -136,4 +136,15 @@ typedef std::map<unsigned,SGBindingList> SGBindingMap;
|
||||
*/
|
||||
void fireBindingList(const SGBindingList& aBindings);
|
||||
|
||||
/**
|
||||
* fire every binding in a list with a setting value
|
||||
|
||||
*/
|
||||
void fireBindingListWithOffset(const SGBindingList& aBindings, double offset, double max);
|
||||
|
||||
/**
|
||||
* read multiple bindings from property-list format
|
||||
*/
|
||||
SGBindingList readBindingList(const simgear::PropertyList& aNodes, SGPropertyNode* aRoot);
|
||||
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user