Implement world ray casting query
This commit is contained in:
parent
e9257ec56f
commit
3da146eb84
|
@ -82,6 +82,7 @@ SET (REACTPHYSICS3D_SOURCES
|
||||||
"src/collision/BroadPhasePair.h"
|
"src/collision/BroadPhasePair.h"
|
||||||
"src/collision/BroadPhasePair.cpp"
|
"src/collision/BroadPhasePair.cpp"
|
||||||
"src/collision/RaycastInfo.h"
|
"src/collision/RaycastInfo.h"
|
||||||
|
"src/collision/RaycastInfo.cpp"
|
||||||
"src/collision/ProxyShape.h"
|
"src/collision/ProxyShape.h"
|
||||||
"src/collision/ProxyShape.cpp"
|
"src/collision/ProxyShape.cpp"
|
||||||
"src/collision/CollisionDetection.h"
|
"src/collision/CollisionDetection.h"
|
||||||
|
|
|
@ -222,6 +222,8 @@ bool CollisionBody::raycast(const Ray& ray, RaycastInfo& raycastInfo) {
|
||||||
// For each collision shape of the body
|
// For each collision shape of the body
|
||||||
for (ProxyShape* shape = mProxyCollisionShapes; shape != NULL; shape = shape->mNext) {
|
for (ProxyShape* shape = mProxyCollisionShapes; shape != NULL; shape = shape->mNext) {
|
||||||
|
|
||||||
|
// TODO : Test for broad-phase hit for each shape before testing actual shape raycast
|
||||||
|
|
||||||
// Test if the ray hits the collision shape
|
// Test if the ray hits the collision shape
|
||||||
if (shape->raycast(rayTemp, raycastInfo)) {
|
if (shape->raycast(rayTemp, raycastInfo)) {
|
||||||
rayTemp.maxFraction = raycastInfo.hitFraction;
|
rayTemp.maxFraction = raycastInfo.hitFraction;
|
||||||
|
|
|
@ -80,8 +80,6 @@ struct BroadPhasePair {
|
||||||
bool operator!=(const BroadPhasePair& broadPhasePair2) const;
|
bool operator!=(const BroadPhasePair& broadPhasePair2) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Return the pair of bodies index
|
// Return the pair of bodies index
|
||||||
inline bodyindexpair BroadPhasePair::getBodiesIndexPair() const {
|
inline bodyindexpair BroadPhasePair::getBodiesIndexPair() const {
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,9 @@ class CollisionDetection {
|
||||||
/// Compute the collision detection
|
/// Compute the collision detection
|
||||||
void computeCollisionDetection();
|
void computeCollisionDetection();
|
||||||
|
|
||||||
|
/// Ray casting method
|
||||||
|
void raycast(RaycastCallback* raycastCallback, const Ray& ray) const;
|
||||||
|
|
||||||
/// Allow the broadphase to notify the collision detection about an overlapping pair.
|
/// Allow the broadphase to notify the collision detection about an overlapping pair.
|
||||||
void broadPhaseNotifyOverlappingPair(ProxyShape* shape1, ProxyShape* shape2);
|
void broadPhaseNotifyOverlappingPair(ProxyShape* shape1, ProxyShape* shape2);
|
||||||
|
|
||||||
|
@ -200,6 +203,17 @@ inline void CollisionDetection::updateProxyCollisionShape(ProxyShape* shape, con
|
||||||
mBroadPhaseAlgorithm.updateProxyCollisionShape(shape, aabb, displacement);
|
mBroadPhaseAlgorithm.updateProxyCollisionShape(shape, aabb, displacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ray casting method
|
||||||
|
inline void CollisionDetection::raycast(RaycastCallback* raycastCallback,
|
||||||
|
const Ray& ray) const {
|
||||||
|
|
||||||
|
RaycastTest rayCastTest(raycastCallback);
|
||||||
|
|
||||||
|
// Ask the broad-phase algorithm to call the testRaycastAgainstShape()
|
||||||
|
// callback method for each proxy shape hit by the ray in the broad-phase
|
||||||
|
mBroadPhaseAlgorithm.raycast(ray, rayCastTest);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
49
src/collision/RaycastInfo.cpp
Normal file
49
src/collision/RaycastInfo.cpp
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/********************************************************************************
|
||||||
|
* ReactPhysics3D physics library, http://code.google.com/p/reactphysics3d/ *
|
||||||
|
* Copyright (c) 2010-2014 Daniel Chappuis *
|
||||||
|
*********************************************************************************
|
||||||
|
* *
|
||||||
|
* This software is provided 'as-is', without any express or implied warranty. *
|
||||||
|
* In no event will the authors be held liable for any damages arising from the *
|
||||||
|
* use of this software. *
|
||||||
|
* *
|
||||||
|
* Permission is granted to anyone to use this software for any purpose, *
|
||||||
|
* including commercial applications, and to alter it and redistribute it *
|
||||||
|
* freely, subject to the following restrictions: *
|
||||||
|
* *
|
||||||
|
* 1. The origin of this software must not be misrepresented; you must not claim *
|
||||||
|
* that you wrote the original software. If you use this software in a *
|
||||||
|
* product, an acknowledgment in the product documentation would be *
|
||||||
|
* appreciated but is not required. *
|
||||||
|
* *
|
||||||
|
* 2. Altered source versions must be plainly marked as such, and must not be *
|
||||||
|
* misrepresented as being the original software. *
|
||||||
|
* *
|
||||||
|
* 3. This notice may not be removed or altered from any source distribution. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
// Libraries
|
||||||
|
#include "decimal.h"
|
||||||
|
#include "RaycastInfo.h"
|
||||||
|
#include "ProxyShape.h"
|
||||||
|
|
||||||
|
using namespace reactphysics3d;
|
||||||
|
|
||||||
|
// Ray cast test against a proxy shape
|
||||||
|
decimal RaycastTest::raycastAgainstShape(ProxyShape* shape, const Ray& ray) {
|
||||||
|
|
||||||
|
// Ray casting test against the collision shape
|
||||||
|
RaycastInfo raycastInfo;
|
||||||
|
bool isHit = shape->raycast(ray, raycastInfo);
|
||||||
|
|
||||||
|
// If the ray hit the collision shape
|
||||||
|
if (isHit) {
|
||||||
|
|
||||||
|
// Report the hit to the user and return the
|
||||||
|
// user hit fraction value
|
||||||
|
return userCallback->notifyRaycastHit(raycastInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ray.maxFraction;
|
||||||
|
}
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
#include "mathematics/Vector3.h"
|
#include "mathematics/Vector3.h"
|
||||||
|
#include "mathematics/Ray.h"
|
||||||
|
|
||||||
/// ReactPhysics3D namespace
|
/// ReactPhysics3D namespace
|
||||||
namespace reactphysics3d {
|
namespace reactphysics3d {
|
||||||
|
@ -86,6 +87,56 @@ struct RaycastInfo {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Class RaycastCallback
|
||||||
|
/**
|
||||||
|
* This class can be used to register a callback for ray casting queries.
|
||||||
|
* You should implement your own class inherited from this one and implement
|
||||||
|
* the notifyRaycastHit() method. This method will be called for each ProxyShape
|
||||||
|
* that is hit by the ray.
|
||||||
|
*/
|
||||||
|
class RaycastCallback {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
// -------------------- Methods -------------------- //
|
||||||
|
|
||||||
|
/// Destructor
|
||||||
|
virtual ~RaycastCallback() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method will be called for each ProxyShape that is hit by the
|
||||||
|
/// ray. You cannot make any assumptions about the order of the
|
||||||
|
/// calls. You should use the return value to control the continuation
|
||||||
|
/// of the ray. The return value is the next maxFraction value to use.
|
||||||
|
/// If you return a fraction of 0.0, it means that the raycast should
|
||||||
|
/// terminate. If you return a fraction of 1.0, it indicates that the
|
||||||
|
/// ray is not clipped and the ray cast should continue as if no hit
|
||||||
|
/// occurred. If you return the fraction in the parameter (hitFraction
|
||||||
|
/// value in the RaycastInfo object), the current ray will be clipped
|
||||||
|
/// to this fraction in the next queries. If you return -1.0, it will
|
||||||
|
/// ignore this ProxyShape and continue the ray cast.
|
||||||
|
virtual decimal notifyRaycastHit(const RaycastInfo& raycastInfo)=0;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Structure RaycastTest
|
||||||
|
struct RaycastTest {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/// User callback class
|
||||||
|
RaycastCallback* userCallback;
|
||||||
|
|
||||||
|
/// Constructor
|
||||||
|
RaycastTest(RaycastCallback* callback) {
|
||||||
|
userCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ray cast test against a proxy shape
|
||||||
|
decimal raycastAgainstShape(ProxyShape* shape, const Ray& ray);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -157,6 +157,9 @@ class BroadPhaseAlgorithm {
|
||||||
|
|
||||||
/// Return true if the two broad-phase collision shapes are overlapping
|
/// Return true if the two broad-phase collision shapes are overlapping
|
||||||
bool testOverlappingShapes(ProxyShape* shape1, ProxyShape* shape2) const;
|
bool testOverlappingShapes(ProxyShape* shape1, ProxyShape* shape2) const;
|
||||||
|
|
||||||
|
/// Ray casting method
|
||||||
|
void raycast(const Ray& ray, RaycastTest& raycastTest) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Method used to compare two pairs for sorting algorithm
|
// Method used to compare two pairs for sorting algorithm
|
||||||
|
@ -180,6 +183,12 @@ inline bool BroadPhaseAlgorithm::testOverlappingShapes(ProxyShape* shape1,
|
||||||
return aabb1.testCollision(aabb2);
|
return aabb1.testCollision(aabb2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ray casting method
|
||||||
|
inline void BroadPhaseAlgorithm::raycast(const Ray& ray,
|
||||||
|
RaycastTest& raycastTest) const {
|
||||||
|
mDynamicAABBTree.raycast(ray, raycastTest);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -605,6 +605,79 @@ void DynamicAABBTree::reportAllShapesOverlappingWith(int nodeID, const AABB& aab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ray casting method
|
||||||
|
void DynamicAABBTree::raycast(const Ray& ray, RaycastTest& raycastTest) const {
|
||||||
|
|
||||||
|
decimal maxFraction = ray.maxFraction;
|
||||||
|
|
||||||
|
// Create an AABB for the ray
|
||||||
|
Vector3 endPoint = ray.point1 +
|
||||||
|
maxFraction * (ray.point2 - ray.point1);
|
||||||
|
AABB rayAABB(Vector3::min(ray.point1, endPoint),
|
||||||
|
Vector3::max(ray.point1, endPoint));
|
||||||
|
|
||||||
|
Stack<int, 128> stack;
|
||||||
|
stack.push(mRootNodeID);
|
||||||
|
|
||||||
|
// Walk through the tree from the root looking for proxy shapes
|
||||||
|
// that overlap with the ray AABB
|
||||||
|
while (stack.getNbElements() > 0) {
|
||||||
|
|
||||||
|
// Get the next node in the stack
|
||||||
|
int nodeID = stack.pop();
|
||||||
|
|
||||||
|
// If it is a null node, skip it
|
||||||
|
if (nodeID == TreeNode::NULL_TREE_NODE) continue;
|
||||||
|
|
||||||
|
// Get the corresponding node
|
||||||
|
const TreeNode* node = mNodes + nodeID;
|
||||||
|
|
||||||
|
// Test if the node AABB overlaps with the ray AABB
|
||||||
|
if (!rayAABB.testCollision(node->aabb)) continue;
|
||||||
|
|
||||||
|
// If the node is a leaf of the tree
|
||||||
|
if (node->isLeaf()) {
|
||||||
|
|
||||||
|
Ray rayTemp(ray.point1, ray.point2, maxFraction);
|
||||||
|
|
||||||
|
// Ask the collision detection to perform a ray cast test against
|
||||||
|
// the proxy shape of this node because the ray is overlapping
|
||||||
|
// with the shape in the broad-phase
|
||||||
|
decimal hitFraction = raycastTest.raycastAgainstShape(node->proxyShape,
|
||||||
|
rayTemp);
|
||||||
|
|
||||||
|
// If the user returned a hitFraction of zero, it means that
|
||||||
|
// the raycasting should stop here
|
||||||
|
if (hitFraction == decimal(0.0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user returned a positive fraction
|
||||||
|
if (hitFraction > decimal(0.0)) {
|
||||||
|
|
||||||
|
// We update the maxFraction value and the ray
|
||||||
|
// AABB using the new maximum fraction
|
||||||
|
if (hitFraction < maxFraction) {
|
||||||
|
maxFraction = hitFraction;
|
||||||
|
}
|
||||||
|
endPoint = ray.point1 + maxFraction * (ray.point2 - ray.point1);
|
||||||
|
rayAABB.mMinCoordinates = Vector3::min(ray.point1, endPoint);
|
||||||
|
rayAABB.mMaxCoordinates = Vector3::max(ray.point1, endPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user returned a negative fraction, we continue
|
||||||
|
// the raycasting as if the proxy shape did not exist
|
||||||
|
|
||||||
|
}
|
||||||
|
else { // If the node has children
|
||||||
|
|
||||||
|
// Push its children in the stack of nodes to explore
|
||||||
|
stack.push(node->leftChildID);
|
||||||
|
stack.push(node->rightChildID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
|
|
||||||
// Check if the tree structure is valid (for debugging purpose)
|
// Check if the tree structure is valid (for debugging purpose)
|
||||||
|
|
|
@ -36,6 +36,12 @@ namespace reactphysics3d {
|
||||||
|
|
||||||
// Declarations
|
// Declarations
|
||||||
class BroadPhaseAlgorithm;
|
class BroadPhaseAlgorithm;
|
||||||
|
struct RaycastTest;
|
||||||
|
|
||||||
|
// Raycast callback method pointer type
|
||||||
|
typedef decimal (*RaycastTestCallback) (ProxyShape* shape,
|
||||||
|
RaycastCallback* userCallback,
|
||||||
|
const Ray& ray);
|
||||||
|
|
||||||
// Structure TreeNode
|
// Structure TreeNode
|
||||||
/**
|
/**
|
||||||
|
@ -160,6 +166,9 @@ class DynamicAABBTree {
|
||||||
|
|
||||||
/// Report all shapes overlapping with the AABB given in parameter.
|
/// Report all shapes overlapping with the AABB given in parameter.
|
||||||
void reportAllShapesOverlappingWith(int nodeID, const AABB& aabb);
|
void reportAllShapesOverlappingWith(int nodeID, const AABB& aabb);
|
||||||
|
|
||||||
|
/// Ray casting method
|
||||||
|
void raycast(const Ray& ray, RaycastTest& raycastTest) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return true if the node is a leaf of the tree
|
// Return true if the node is a leaf of the tree
|
||||||
|
|
|
@ -166,10 +166,4 @@ void CollisionWorld::removeCollisionShape(CollisionShape* collisionShape) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Raycast method with feedback information
|
|
||||||
bool CollisionWorld::raycast(const Ray& ray, RaycastInfo& raycastInfo) {
|
|
||||||
// TODO : Implement this method
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -120,8 +120,8 @@ class CollisionWorld {
|
||||||
/// Destroy a collision body
|
/// Destroy a collision body
|
||||||
void destroyCollisionBody(CollisionBody* collisionBody);
|
void destroyCollisionBody(CollisionBody* collisionBody);
|
||||||
|
|
||||||
/// Raycast method with feedback information
|
/// Ray cast method
|
||||||
bool raycast(const Ray& ray, RaycastInfo& raycastInfo);
|
void raycast(const Ray& ray, RaycastCallback* raycastCallback) const;
|
||||||
|
|
||||||
// -------------------- Friendship -------------------- //
|
// -------------------- Friendship -------------------- //
|
||||||
|
|
||||||
|
@ -141,6 +141,12 @@ inline std::set<CollisionBody*>::iterator CollisionWorld::getBodiesEndIterator()
|
||||||
return mBodies.end();
|
return mBodies.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ray cast method
|
||||||
|
inline void CollisionWorld::raycast(const Ray& ray,
|
||||||
|
RaycastCallback* raycastCallback) const {
|
||||||
|
mCollisionDetection.raycast(raycastCallback, ray);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -132,6 +132,12 @@ struct Vector2 {
|
||||||
/// Overloaded operator
|
/// Overloaded operator
|
||||||
Vector2& operator=(const Vector2& vector);
|
Vector2& operator=(const Vector2& vector);
|
||||||
|
|
||||||
|
/// Return a vector taking the minimum components of two vectors
|
||||||
|
static Vector2 min(const Vector2& vector1, const Vector2& vector2);
|
||||||
|
|
||||||
|
/// Return a vector taking the maximum components of two vectors
|
||||||
|
static Vector2 max(const Vector2& vector1, const Vector2& vector2);
|
||||||
|
|
||||||
// -------------------- Friends -------------------- //
|
// -------------------- Friends -------------------- //
|
||||||
|
|
||||||
friend Vector2 operator+(const Vector2& vector1, const Vector2& vector2);
|
friend Vector2 operator+(const Vector2& vector1, const Vector2& vector2);
|
||||||
|
@ -291,6 +297,18 @@ inline Vector2& Vector2::operator=(const Vector2& vector) {
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return a vector taking the minimum components of two vectors
|
||||||
|
inline Vector2 Vector2::min(const Vector2& vector1, const Vector2& vector2) {
|
||||||
|
return Vector2(std::min(vector1.x, vector2.x),
|
||||||
|
std::min(vector1.y, vector2.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a vector taking the maximum components of two vectors
|
||||||
|
inline Vector2 Vector2::max(const Vector2& vector1, const Vector2& vector2) {
|
||||||
|
return Vector2(std::max(vector1.x, vector2.x),
|
||||||
|
std::max(vector1.y, vector2.y));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -138,6 +138,12 @@ struct Vector3 {
|
||||||
/// Overloaded operator
|
/// Overloaded operator
|
||||||
Vector3& operator=(const Vector3& vector);
|
Vector3& operator=(const Vector3& vector);
|
||||||
|
|
||||||
|
/// Return a vector taking the minimum components of two vectors
|
||||||
|
static Vector3 min(const Vector3& vector1, const Vector3& vector2);
|
||||||
|
|
||||||
|
/// Return a vector taking the maximum components of two vectors
|
||||||
|
static Vector3 max(const Vector3& vector1, const Vector3& vector2);
|
||||||
|
|
||||||
// -------------------- Friends -------------------- //
|
// -------------------- Friends -------------------- //
|
||||||
|
|
||||||
friend Vector3 operator+(const Vector3& vector1, const Vector3& vector2);
|
friend Vector3 operator+(const Vector3& vector1, const Vector3& vector2);
|
||||||
|
@ -312,6 +318,20 @@ inline Vector3& Vector3::operator=(const Vector3& vector) {
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return a vector taking the minimum components of two vectors
|
||||||
|
inline Vector3 Vector3::min(const Vector3& vector1, const Vector3& vector2) {
|
||||||
|
return Vector3(std::min(vector1.x, vector2.x),
|
||||||
|
std::min(vector1.y, vector2.y),
|
||||||
|
std::min(vector1.z, vector2.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a vector taking the maximum components of two vectors
|
||||||
|
inline Vector3 Vector3::max(const Vector3& vector1, const Vector3& vector2) {
|
||||||
|
return Vector3(std::max(vector1.x, vector2.x),
|
||||||
|
std::max(vector1.y, vector2.y),
|
||||||
|
std::max(vector1.z, vector2.z));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -156,6 +156,12 @@ class TestVector2 : public Test {
|
||||||
test(Vector2(7, 537).getMaxAxis() == 1);
|
test(Vector2(7, 537).getMaxAxis() == 1);
|
||||||
test(Vector2(98, 23).getMaxAxis() == 0);
|
test(Vector2(98, 23).getMaxAxis() == 0);
|
||||||
test(Vector2(-53, -25).getMaxAxis() == 1);
|
test(Vector2(-53, -25).getMaxAxis() == 1);
|
||||||
|
|
||||||
|
// Test the methot that return a max/min vector
|
||||||
|
Vector2 vec1(-5, 4);
|
||||||
|
Vector2 vec2(-8, 6);
|
||||||
|
test(Vector2::min(vec1, vec2) == Vector2(-8, 4));
|
||||||
|
test(Vector2::max(vec1, vec2) == Vector2(-5, 6));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the operators
|
/// Test the operators
|
||||||
|
|
|
@ -178,6 +178,12 @@ class TestVector3 : public Test {
|
||||||
test(Vector3(7, 533, 36).getMaxAxis() == 1);
|
test(Vector3(7, 533, 36).getMaxAxis() == 1);
|
||||||
test(Vector3(98, 23, 3).getMaxAxis() == 0);
|
test(Vector3(98, 23, 3).getMaxAxis() == 0);
|
||||||
test(Vector3(-53, -25, -63).getMaxAxis() == 1);
|
test(Vector3(-53, -25, -63).getMaxAxis() == 1);
|
||||||
|
|
||||||
|
// Test the methot that return a max/min vector
|
||||||
|
Vector3 vec1(-5, 4, 2);
|
||||||
|
Vector3 vec2(-8, 6, -1);
|
||||||
|
test(Vector3::min(vec1, vec2) == Vector3(-8, 4, -1));
|
||||||
|
test(Vector3::max(vec1, vec2) == Vector3(-5, 6, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the operators
|
/// Test the operators
|
||||||
|
|
Loading…
Reference in New Issue
Block a user