forked from LeenkxTeam/LNXSDK
		
	
		
			
	
	
		
			468 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
		
		
			
		
	
	
			468 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
|  | /*
 | ||
|  | Bullet Continuous Collision Detection and Physics Library | ||
|  | Copyright (c) 2003-2006 Erwin Coumans  http://continuousphysics.com/Bullet/
 | ||
|  | 
 | ||
|  | 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. | ||
|  | */ | ||
|  | 
 | ||
|  | #include "btGjkPairDetector.h"
 | ||
|  | #include "BulletCollision/CollisionShapes/btConvexShape.h"
 | ||
|  | #include "BulletCollision/NarrowPhaseCollision/btSimplexSolverInterface.h"
 | ||
|  | #include "BulletCollision/NarrowPhaseCollision/btConvexPenetrationDepthSolver.h"
 | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | #if defined(DEBUG) || defined (_DEBUG)
 | ||
|  | //#define TEST_NON_VIRTUAL 1
 | ||
|  | #include <stdio.h> //for debug printf
 | ||
|  | #ifdef __SPU__
 | ||
|  | #include <spu_printf.h>
 | ||
|  | #define printf spu_printf
 | ||
|  | #endif //__SPU__
 | ||
|  | #endif
 | ||
|  | 
 | ||
|  | //must be above the machine epsilon
 | ||
|  | #ifdef  BT_USE_DOUBLE_PRECISION
 | ||
|  | 	#define REL_ERROR2 btScalar(1.0e-12)
 | ||
|  | 	btScalar gGjkEpaPenetrationTolerance = 1e-7; | ||
|  | #else
 | ||
|  | 	#define REL_ERROR2 btScalar(1.0e-6)
 | ||
|  | 	btScalar gGjkEpaPenetrationTolerance = 0.001; | ||
|  | #endif
 | ||
|  | 
 | ||
|  | //temp globals, to improve GJK/EPA/penetration calculations
 | ||
|  | int gNumDeepPenetrationChecks = 0; | ||
|  | int gNumGjkChecks = 0; | ||
|  | 
 | ||
|  | 
 | ||
|  | btGjkPairDetector::btGjkPairDetector(const btConvexShape* objectA,const btConvexShape* objectB,btSimplexSolverInterface* simplexSolver,btConvexPenetrationDepthSolver*	penetrationDepthSolver) | ||
|  | :m_cachedSeparatingAxis(btScalar(0.),btScalar(1.),btScalar(0.)), | ||
|  | m_penetrationDepthSolver(penetrationDepthSolver), | ||
|  | m_simplexSolver(simplexSolver), | ||
|  | m_minkowskiA(objectA), | ||
|  | m_minkowskiB(objectB), | ||
|  | m_shapeTypeA(objectA->getShapeType()), | ||
|  | m_shapeTypeB(objectB->getShapeType()), | ||
|  | m_marginA(objectA->getMargin()), | ||
|  | m_marginB(objectB->getMargin()), | ||
|  | m_ignoreMargin(false), | ||
|  | m_lastUsedMethod(-1), | ||
|  | m_catchDegeneracies(1), | ||
|  | m_fixContactNormalDirection(1) | ||
|  | { | ||
|  | } | ||
|  | btGjkPairDetector::btGjkPairDetector(const btConvexShape* objectA,const btConvexShape* objectB,int shapeTypeA,int shapeTypeB,btScalar marginA, btScalar marginB, btSimplexSolverInterface* simplexSolver,btConvexPenetrationDepthSolver*	penetrationDepthSolver) | ||
|  | :m_cachedSeparatingAxis(btScalar(0.),btScalar(1.),btScalar(0.)), | ||
|  | m_penetrationDepthSolver(penetrationDepthSolver), | ||
|  | m_simplexSolver(simplexSolver), | ||
|  | m_minkowskiA(objectA), | ||
|  | m_minkowskiB(objectB), | ||
|  | m_shapeTypeA(shapeTypeA), | ||
|  | m_shapeTypeB(shapeTypeB), | ||
|  | m_marginA(marginA), | ||
|  | m_marginB(marginB), | ||
|  | m_ignoreMargin(false), | ||
|  | m_lastUsedMethod(-1), | ||
|  | m_catchDegeneracies(1), | ||
|  | m_fixContactNormalDirection(1) | ||
|  | { | ||
|  | } | ||
|  | 
 | ||
|  | void	btGjkPairDetector::getClosestPoints(const ClosestPointInput& input,Result& output,class btIDebugDraw* debugDraw,bool swapResults) | ||
|  | { | ||
|  | 	(void)swapResults; | ||
|  | 
 | ||
|  | 	getClosestPointsNonVirtual(input,output,debugDraw); | ||
|  | } | ||
|  | 
 | ||
|  | #ifdef __SPU__
 | ||
|  | void btGjkPairDetector::getClosestPointsNonVirtual(const ClosestPointInput& input,Result& output,class btIDebugDraw* debugDraw) | ||
|  | #else
 | ||
|  | void btGjkPairDetector::getClosestPointsNonVirtual(const ClosestPointInput& input, Result& output, class btIDebugDraw* debugDraw) | ||
|  | #endif
 | ||
|  | { | ||
|  | 	m_cachedSeparatingDistance = 0.f; | ||
|  | 
 | ||
|  | 	btScalar distance=btScalar(0.); | ||
|  | 	btVector3	normalInB(btScalar(0.),btScalar(0.),btScalar(0.)); | ||
|  | 
 | ||
|  | 	btVector3 pointOnA,pointOnB; | ||
|  | 	btTransform	localTransA = input.m_transformA; | ||
|  | 	btTransform localTransB = input.m_transformB; | ||
|  | 	btVector3 positionOffset=(localTransA.getOrigin() + localTransB.getOrigin()) * btScalar(0.5); | ||
|  | 	localTransA.getOrigin() -= positionOffset; | ||
|  | 	localTransB.getOrigin() -= positionOffset; | ||
|  | 
 | ||
|  | 	bool check2d = m_minkowskiA->isConvex2d() && m_minkowskiB->isConvex2d(); | ||
|  | 
 | ||
|  | 	btScalar marginA = m_marginA; | ||
|  | 	btScalar marginB = m_marginB; | ||
|  | 
 | ||
|  | 	gNumGjkChecks++; | ||
|  | 
 | ||
|  | 	//for CCD we don't use margins
 | ||
|  | 	if (m_ignoreMargin) | ||
|  | 	{ | ||
|  | 		marginA = btScalar(0.); | ||
|  | 		marginB = btScalar(0.); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	m_curIter = 0; | ||
|  | 	int gGjkMaxIter = 1000;//this is to catch invalid input, perhaps check for #NaN?
 | ||
|  | 	m_cachedSeparatingAxis.setValue(0,1,0); | ||
|  | 
 | ||
|  | 	bool isValid = false; | ||
|  | 	bool checkSimplex = false; | ||
|  | 	bool checkPenetration = true; | ||
|  | 	m_degenerateSimplex = 0; | ||
|  | 
 | ||
|  | 	m_lastUsedMethod = -1; | ||
|  | 
 | ||
|  | 	{ | ||
|  | 		btScalar squaredDistance = BT_LARGE_FLOAT; | ||
|  | 		btScalar delta = btScalar(0.); | ||
|  | 		 | ||
|  | 		btScalar margin = marginA + marginB; | ||
|  | 		 | ||
|  | 		 | ||
|  | 
 | ||
|  | 		m_simplexSolver->reset(); | ||
|  | 		 | ||
|  | 		for ( ; ; ) | ||
|  | 		//while (true)
 | ||
|  | 		{ | ||
|  | 
 | ||
|  | 			btVector3 seperatingAxisInA = (-m_cachedSeparatingAxis)* input.m_transformA.getBasis(); | ||
|  | 			btVector3 seperatingAxisInB = m_cachedSeparatingAxis* input.m_transformB.getBasis(); | ||
|  | 
 | ||
|  | 
 | ||
|  | 			btVector3 pInA = m_minkowskiA->localGetSupportVertexWithoutMarginNonVirtual(seperatingAxisInA); | ||
|  | 			btVector3 qInB = m_minkowskiB->localGetSupportVertexWithoutMarginNonVirtual(seperatingAxisInB); | ||
|  | 
 | ||
|  | 			btVector3  pWorld = localTransA(pInA);	 | ||
|  | 			btVector3  qWorld = localTransB(qInB); | ||
|  | 
 | ||
|  | 
 | ||
|  | 			if (check2d) | ||
|  | 			{ | ||
|  | 				pWorld[2] = 0.f; | ||
|  | 				qWorld[2] = 0.f; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			btVector3 w	= pWorld - qWorld; | ||
|  | 			delta = m_cachedSeparatingAxis.dot(w); | ||
|  | 
 | ||
|  | 			// potential exit, they don't overlap
 | ||
|  | 			if ((delta > btScalar(0.0)) && (delta * delta > squaredDistance * input.m_maximumDistanceSquared))  | ||
|  | 			{ | ||
|  | 				m_degenerateSimplex = 10; | ||
|  | 				checkSimplex=true; | ||
|  | 				//checkPenetration = false;
 | ||
|  | 				break; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			//exit 0: the new point is already in the simplex, or we didn't come any closer
 | ||
|  | 			if (m_simplexSolver->inSimplex(w)) | ||
|  | 			{ | ||
|  | 				m_degenerateSimplex = 1; | ||
|  | 				checkSimplex = true; | ||
|  | 				break; | ||
|  | 			} | ||
|  | 			// are we getting any closer ?
 | ||
|  | 			btScalar f0 = squaredDistance - delta; | ||
|  | 			btScalar f1 = squaredDistance * REL_ERROR2; | ||
|  | 
 | ||
|  | 			if (f0 <= f1) | ||
|  | 			{ | ||
|  | 				if (f0 <= btScalar(0.)) | ||
|  | 				{ | ||
|  | 					m_degenerateSimplex = 2; | ||
|  | 				} else | ||
|  | 				{ | ||
|  | 					m_degenerateSimplex = 11; | ||
|  | 				} | ||
|  | 				checkSimplex = true; | ||
|  | 				break; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			//add current vertex to simplex
 | ||
|  | 			m_simplexSolver->addVertex(w, pWorld, qWorld); | ||
|  | 			btVector3 newCachedSeparatingAxis; | ||
|  | 
 | ||
|  | 			//calculate the closest point to the origin (update vector v)
 | ||
|  | 			if (!m_simplexSolver->closest(newCachedSeparatingAxis)) | ||
|  | 			{ | ||
|  | 				m_degenerateSimplex = 3; | ||
|  | 				checkSimplex = true; | ||
|  | 				break; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if(newCachedSeparatingAxis.length2()<REL_ERROR2) | ||
|  |             { | ||
|  | 				m_cachedSeparatingAxis = newCachedSeparatingAxis; | ||
|  |                 m_degenerateSimplex = 6; | ||
|  |                 checkSimplex = true; | ||
|  |                 break; | ||
|  |             } | ||
|  | 
 | ||
|  | 			btScalar previousSquaredDistance = squaredDistance; | ||
|  | 			squaredDistance = newCachedSeparatingAxis.length2(); | ||
|  | #if 0
 | ||
|  | ///warning: this termination condition leads to some problems in 2d test case see Bullet/Demos/Box2dDemo
 | ||
|  | 			if (squaredDistance>previousSquaredDistance) | ||
|  | 			{ | ||
|  | 				m_degenerateSimplex = 7; | ||
|  | 				squaredDistance = previousSquaredDistance; | ||
|  |                 checkSimplex = false; | ||
|  |                 break; | ||
|  | 			} | ||
|  | #endif //
 | ||
|  | 			 | ||
|  | 
 | ||
|  | 			//redundant m_simplexSolver->compute_points(pointOnA, pointOnB);
 | ||
|  | 
 | ||
|  | 			//are we getting any closer ?
 | ||
|  | 			if (previousSquaredDistance - squaredDistance <= SIMD_EPSILON * previousSquaredDistance)  | ||
|  | 			{  | ||
|  | //				m_simplexSolver->backup_closest(m_cachedSeparatingAxis);
 | ||
|  | 				checkSimplex = true; | ||
|  | 				m_degenerateSimplex = 12; | ||
|  | 				 | ||
|  | 				break; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			m_cachedSeparatingAxis = newCachedSeparatingAxis; | ||
|  | 
 | ||
|  | 			  //degeneracy, this is typically due to invalid/uninitialized worldtransforms for a btCollisionObject   
 | ||
|  |               if (m_curIter++ > gGjkMaxIter)    | ||
|  |               {    | ||
|  |                       #if defined(DEBUG) || defined (_DEBUG)
 | ||
|  | 
 | ||
|  |                               printf("btGjkPairDetector maxIter exceeded:%i\n",m_curIter);    | ||
|  |                               printf("sepAxis=(%f,%f,%f), squaredDistance = %f, shapeTypeA=%i,shapeTypeB=%i\n",    | ||
|  |                               m_cachedSeparatingAxis.getX(),    | ||
|  |                               m_cachedSeparatingAxis.getY(),    | ||
|  |                               m_cachedSeparatingAxis.getZ(),    | ||
|  |                               squaredDistance,    | ||
|  |                               m_minkowskiA->getShapeType(),    | ||
|  |                               m_minkowskiB->getShapeType());    | ||
|  | 
 | ||
|  |                       #endif   
 | ||
|  |                       break;    | ||
|  | 
 | ||
|  |               }  | ||
|  | 
 | ||
|  | 
 | ||
|  | 			bool check = (!m_simplexSolver->fullSimplex()); | ||
|  | 			//bool check = (!m_simplexSolver->fullSimplex() && squaredDistance > SIMD_EPSILON * m_simplexSolver->maxVertex());
 | ||
|  | 
 | ||
|  | 			if (!check) | ||
|  | 			{ | ||
|  | 				//do we need this backup_closest here ?
 | ||
|  | //				m_simplexSolver->backup_closest(m_cachedSeparatingAxis);
 | ||
|  | 				m_degenerateSimplex = 13; | ||
|  | 				break; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if (checkSimplex) | ||
|  | 		{ | ||
|  | 			m_simplexSolver->compute_points(pointOnA, pointOnB); | ||
|  | 			normalInB = m_cachedSeparatingAxis; | ||
|  | 
 | ||
|  | 			btScalar lenSqr =m_cachedSeparatingAxis.length2(); | ||
|  | 			 | ||
|  | 			//valid normal
 | ||
|  | 			if (lenSqr < REL_ERROR2) | ||
|  | 			{ | ||
|  | 				m_degenerateSimplex = 5; | ||
|  | 			}  | ||
|  | 			if (lenSqr > SIMD_EPSILON*SIMD_EPSILON) | ||
|  | 			{ | ||
|  | 				btScalar rlen = btScalar(1.) / btSqrt(lenSqr ); | ||
|  | 				normalInB *= rlen; //normalize
 | ||
|  | 
 | ||
|  | 				btScalar s = btSqrt(squaredDistance); | ||
|  | 			 | ||
|  | 				btAssert(s > btScalar(0.0)); | ||
|  | 				pointOnA -= m_cachedSeparatingAxis * (marginA / s); | ||
|  | 				pointOnB += m_cachedSeparatingAxis * (marginB / s); | ||
|  | 				distance = ((btScalar(1.)/rlen) - margin); | ||
|  | 				isValid = true; | ||
|  | 				 | ||
|  | 				m_lastUsedMethod = 1; | ||
|  | 			} else | ||
|  | 			{ | ||
|  | 				m_lastUsedMethod = 2; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		bool catchDegeneratePenetrationCase =  | ||
|  | 			(m_catchDegeneracies && m_penetrationDepthSolver && m_degenerateSimplex && ((distance+margin) < gGjkEpaPenetrationTolerance)); | ||
|  | 
 | ||
|  | 		//if (checkPenetration && !isValid)
 | ||
|  | 		if (checkPenetration && (!isValid || catchDegeneratePenetrationCase )) | ||
|  | 		{ | ||
|  | 			//penetration case
 | ||
|  | 
 | ||
|  | 			//if there is no way to handle penetrations, bail out
 | ||
|  | 			if (m_penetrationDepthSolver) | ||
|  | 			{ | ||
|  | 				// Penetration depth case.
 | ||
|  | 				btVector3 tmpPointOnA,tmpPointOnB; | ||
|  | 				 | ||
|  | 				gNumDeepPenetrationChecks++; | ||
|  | 				m_cachedSeparatingAxis.setZero(); | ||
|  | 
 | ||
|  | 				bool isValid2 = m_penetrationDepthSolver->calcPenDepth(  | ||
|  | 					*m_simplexSolver,  | ||
|  | 					m_minkowskiA,m_minkowskiB, | ||
|  | 					localTransA,localTransB, | ||
|  | 					m_cachedSeparatingAxis, tmpPointOnA, tmpPointOnB, | ||
|  | 					debugDraw | ||
|  | 					); | ||
|  | 
 | ||
|  | 
 | ||
|  | 				if (isValid2) | ||
|  | 				{ | ||
|  | 					btVector3 tmpNormalInB = tmpPointOnB-tmpPointOnA; | ||
|  | 					btScalar lenSqr = tmpNormalInB.length2(); | ||
|  | 					if (lenSqr <= (SIMD_EPSILON*SIMD_EPSILON)) | ||
|  | 					{ | ||
|  | 						tmpNormalInB = m_cachedSeparatingAxis; | ||
|  | 						lenSqr = m_cachedSeparatingAxis.length2(); | ||
|  | 					} | ||
|  | 
 | ||
|  | 					if (lenSqr > (SIMD_EPSILON*SIMD_EPSILON)) | ||
|  | 					{ | ||
|  | 						tmpNormalInB /= btSqrt(lenSqr); | ||
|  | 						btScalar distance2 = -(tmpPointOnA-tmpPointOnB).length(); | ||
|  | 						m_lastUsedMethod = 3; | ||
|  | 						//only replace valid penetrations when the result is deeper (check)
 | ||
|  | 						if (!isValid || (distance2 < distance)) | ||
|  | 						{ | ||
|  | 							distance = distance2; | ||
|  | 							pointOnA = tmpPointOnA; | ||
|  | 							pointOnB = tmpPointOnB; | ||
|  | 							normalInB = tmpNormalInB; | ||
|  | 							 | ||
|  | 							isValid = true; | ||
|  | 							 | ||
|  | 						} else | ||
|  | 						{ | ||
|  | 							m_lastUsedMethod = 8; | ||
|  | 						} | ||
|  | 					} else | ||
|  | 					{ | ||
|  | 						m_lastUsedMethod = 9; | ||
|  | 					} | ||
|  | 				} else | ||
|  | 
 | ||
|  | 				{ | ||
|  | 					///this is another degenerate case, where the initial GJK calculation reports a degenerate case
 | ||
|  | 					///EPA reports no penetration, and the second GJK (using the supporting vector without margin)
 | ||
|  | 					///reports a valid positive distance. Use the results of the second GJK instead of failing.
 | ||
|  | 					///thanks to Jacob.Langford for the reproduction case
 | ||
|  | 					///http://code.google.com/p/bullet/issues/detail?id=250
 | ||
|  | 
 | ||
|  | 				 | ||
|  | 					if (m_cachedSeparatingAxis.length2() > btScalar(0.)) | ||
|  | 					{ | ||
|  | 						btScalar distance2 = (tmpPointOnA-tmpPointOnB).length()-margin; | ||
|  | 						//only replace valid distances when the distance is less
 | ||
|  | 						if (!isValid || (distance2 < distance)) | ||
|  | 						{ | ||
|  | 							distance = distance2; | ||
|  | 							pointOnA = tmpPointOnA; | ||
|  | 							pointOnB = tmpPointOnB; | ||
|  | 							pointOnA -= m_cachedSeparatingAxis * marginA ; | ||
|  | 							pointOnB += m_cachedSeparatingAxis * marginB ; | ||
|  | 							normalInB = m_cachedSeparatingAxis; | ||
|  | 							normalInB.normalize(); | ||
|  | 
 | ||
|  | 							isValid = true; | ||
|  | 							m_lastUsedMethod = 6; | ||
|  | 						} else | ||
|  | 						{ | ||
|  | 							m_lastUsedMethod = 5; | ||
|  | 						} | ||
|  | 					} | ||
|  | 				} | ||
|  | 				 | ||
|  | 			} | ||
|  | 
 | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	 | ||
|  | 
 | ||
|  | 	if (isValid && ((distance < 0) || (distance*distance < input.m_maximumDistanceSquared))) | ||
|  | 	{ | ||
|  | 
 | ||
|  | 		m_cachedSeparatingAxis = normalInB; | ||
|  | 		m_cachedSeparatingDistance = distance; | ||
|  | 
 | ||
|  | 		{ | ||
|  | 		///todo: need to track down this EPA penetration solver degeneracy
 | ||
|  | 		///the penetration solver reports penetration but the contact normal
 | ||
|  | 		///connecting the contact points is pointing in the opposite direction
 | ||
|  | 		///until then, detect the issue and revert the normal
 | ||
|  | 
 | ||
|  | 			btScalar d1=0; | ||
|  | 			{ | ||
|  | 				btVector3 seperatingAxisInA = (normalInB)* input.m_transformA.getBasis(); | ||
|  | 				btVector3 seperatingAxisInB = -normalInB* input.m_transformB.getBasis(); | ||
|  | 			 | ||
|  | 
 | ||
|  | 				btVector3 pInA = m_minkowskiA->localGetSupportVertexWithoutMarginNonVirtual(seperatingAxisInA); | ||
|  | 				btVector3 qInB = m_minkowskiB->localGetSupportVertexWithoutMarginNonVirtual(seperatingAxisInB); | ||
|  | 
 | ||
|  | 				btVector3  pWorld = localTransA(pInA);	 | ||
|  | 				btVector3  qWorld = localTransB(qInB); | ||
|  | 				btVector3 w	= pWorld - qWorld; | ||
|  | 				d1 = (-normalInB).dot(w); | ||
|  | 			} | ||
|  | 			btScalar d0 = 0.f; | ||
|  | 			{ | ||
|  | 				btVector3 seperatingAxisInA = (-normalInB)* input.m_transformA.getBasis(); | ||
|  | 				btVector3 seperatingAxisInB = normalInB* input.m_transformB.getBasis(); | ||
|  | 			 | ||
|  | 
 | ||
|  | 				btVector3 pInA = m_minkowskiA->localGetSupportVertexWithoutMarginNonVirtual(seperatingAxisInA); | ||
|  | 				btVector3 qInB = m_minkowskiB->localGetSupportVertexWithoutMarginNonVirtual(seperatingAxisInB); | ||
|  | 
 | ||
|  | 				btVector3  pWorld = localTransA(pInA);	 | ||
|  | 				btVector3  qWorld = localTransB(qInB); | ||
|  | 				btVector3 w	= pWorld - qWorld; | ||
|  | 				d0 = normalInB.dot(w); | ||
|  | 			} | ||
|  | 			if (d1>d0) | ||
|  | 			{ | ||
|  | 				m_lastUsedMethod = 10; | ||
|  | 				normalInB*=-1; | ||
|  | 			}  | ||
|  | 
 | ||
|  | 		} | ||
|  | 		output.addContactPoint( | ||
|  | 			normalInB, | ||
|  | 			pointOnB+positionOffset, | ||
|  | 			distance); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 |