536 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C#
		
	
	
	
		
		
			
		
	
	
			536 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C#
		
	
	
	
|  | /*********************************************** | |||
|  | 				EasyTouch Controls | |||
|  | 	Copyright © 2016 The Hedgehog Team | |||
|  |       http://www.thehedgehogteam.com/Forum/ | |||
|  | 		 | |||
|  | 	  The.Hedgehog.Team@gmail.com | |||
|  | 		 | |||
|  | **********************************************/ | |||
|  | using UnityEngine; | |||
|  | using System.Collections; | |||
|  | 
 | |||
|  | [System.Serializable] | |||
|  | public class ETCAxis { | |||
|  | 
 | |||
|  | 	#region Enumeration | |||
|  | 	public enum DirectAction {Rotate, RotateLocal,Translate, TranslateLocal, Scale, Force,RelativeForce, Torque,RelativeTorque, Jump}; | |||
|  | 	public enum AxisInfluenced{X,Y,Z}; | |||
|  | 	public enum AxisValueMethod {Classical, Curve}; | |||
|  | 	public enum AxisState {None,Down,Press,Up, DownUp,DownDown,DownLeft,DownRight, PressUp, PressDown, PressLeft, PressRight}; | |||
|  | 	public enum ActionOn {Down,Press}; | |||
|  | 	#endregion | |||
|  | 
 | |||
|  | 	#region Members | |||
|  | 	public string name; | |||
|  | 
 | |||
|  | 	public bool autoLinkTagPlayer = false; | |||
|  | 	public string autoTag ="Player"; | |||
|  | 	public GameObject player; | |||
|  | 
 | |||
|  | 	public bool enable; | |||
|  | 	public bool invertedAxis; | |||
|  | 	public float speed; | |||
|  | 
 | |||
|  | 	// | |||
|  | 	public float deadValue; | |||
|  | 	public AxisValueMethod valueMethod; | |||
|  | 	public AnimationCurve curveValue; | |||
|  | 
 | |||
|  | 	public bool isEnertia; | |||
|  | 	public float inertia; | |||
|  | 	public float inertiaThreshold; | |||
|  | 
 | |||
|  | 	// Auto stabilization | |||
|  | 	public bool isAutoStab; | |||
|  | 	public float autoStabThreshold; | |||
|  | 	public float autoStabSpeed; | |||
|  | 	private float startAngle; | |||
|  | 
 | |||
|  | 	// Clamp rotation | |||
|  | 	public bool isClampRotation; | |||
|  | 	public float maxAngle; | |||
|  | 	public float minAngle; | |||
|  | 
 | |||
|  | 	// time push | |||
|  | 	public bool isValueOverTime; | |||
|  | 	public float overTimeStep; | |||
|  | 	public float maxOverTimeValue; | |||
|  | 
 | |||
|  | 	// AvisValue | |||
|  | 	public float axisValue; | |||
|  | 	public float axisSpeedValue; | |||
|  | 	public float axisThreshold; | |||
|  | 	public bool isLockinJump=false; | |||
|  | 	private Vector3 lastMove; | |||
|  | 
 | |||
|  | 	public AxisState axisState; | |||
|  | 
 | |||
|  | 	[SerializeField] | |||
|  | 	private Transform _directTransform; | |||
|  | 	public Transform directTransform { | |||
|  | 		get { | |||
|  | 			return _directTransform; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		set { | |||
|  | 			_directTransform = value; | |||
|  | 			if (_directTransform!=null){ | |||
|  | 				directCharacterController = _directTransform.GetComponent<CharacterController>(); | |||
|  | 				directRigidBody = _directTransform.GetComponent<Rigidbody>(); | |||
|  | 
 | |||
|  | 			} | |||
|  | 			else{ | |||
|  | 				directCharacterController=null;	 | |||
|  | 			} | |||
|  | 
 | |||
|  | 		} | |||
|  | 	} | |||
|  | 	 | |||
|  | 	public DirectAction directAction; | |||
|  | 	public AxisInfluenced axisInfluenced; | |||
|  | 	public ActionOn actionOn; | |||
|  | 
 | |||
|  | 	public CharacterController directCharacterController; | |||
|  | 	public Rigidbody directRigidBody; | |||
|  | 
 | |||
|  | 	public float gravity; | |||
|  | 	public float currentGravity=0; | |||
|  | 	public bool isJump = false; | |||
|  | 
 | |||
|  | 	// Simulation | |||
|  | 	public string unityAxis; | |||
|  | 
 | |||
|  | 	public bool showGeneralInspector=false; | |||
|  | 	public bool showDirectInspector=false; | |||
|  | 	public bool showInertiaInspector=false; | |||
|  | 	public bool showSimulatinInspector=false; | |||
|  | 	#endregion | |||
|  | 
 | |||
|  | 	#region Constructeur | |||
|  | 	public ETCAxis(string axisName){ | |||
|  | 		name = axisName; | |||
|  | 		enable = true; | |||
|  | 		speed = 15; | |||
|  | 		invertedAxis = false; | |||
|  | 		isEnertia = false; | |||
|  | 		inertia = 0; | |||
|  | 		inertiaThreshold = 0.08f; | |||
|  | 		axisValue = 0; | |||
|  | 		axisSpeedValue = 0; | |||
|  | 		gravity = 0; | |||
|  | 		isAutoStab = false; | |||
|  | 		autoStabThreshold = 0.01f; | |||
|  | 		autoStabSpeed = 10; | |||
|  | 		maxAngle = 90; | |||
|  | 		minAngle = 90; | |||
|  | 		axisState = AxisState.None; | |||
|  | 		maxOverTimeValue = 1; | |||
|  | 		overTimeStep = 1; | |||
|  | 		isValueOverTime = false; | |||
|  | 		axisThreshold = 0.5f; | |||
|  | 		deadValue = 0.1f; | |||
|  | 		actionOn = ActionOn.Press; | |||
|  | 	} | |||
|  | 	#endregion | |||
|  | 
 | |||
|  | 	#region Public method | |||
|  | 	public void InitAxis(){ | |||
|  | 
 | |||
|  | 		if (autoLinkTagPlayer){ | |||
|  | 
 | |||
|  | 			player = GameObject.FindGameObjectWithTag(autoTag); | |||
|  | 			if (player){ | |||
|  | 				directTransform = player.transform; | |||
|  | 			} | |||
|  | 		} | |||
|  | 		startAngle = GetAngle(); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	public void UpdateAxis(float realValue, bool isOnDrag, ETCBase.ControlType type,bool deltaTime=true){ | |||
|  | 
 | |||
|  | 		// Auto link | |||
|  | 		if (autoLinkTagPlayer && player==null || ( player && !player.activeSelf)){ | |||
|  | 			player = GameObject.FindGameObjectWithTag(autoTag); | |||
|  | 			if (player){ | |||
|  | 				directTransform = player.transform; | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 
 | |||
|  | 		// Auto stabilization | |||
|  | 		if (isAutoStab && axisValue ==0 && _directTransform){ | |||
|  | 			DoAutoStabilisation(); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		if (invertedAxis){realValue *= -1;} | |||
|  | 
 | |||
|  | 		// Time push | |||
|  | 		if (isValueOverTime && realValue!=0){ | |||
|  | 
 | |||
|  | 			axisValue += overTimeStep * Mathf.Sign(realValue ) * Time.deltaTime; | |||
|  | 
 | |||
|  | 			if (Mathf.Sign(axisValue )>0){ | |||
|  | 				axisValue = Mathf.Clamp( axisValue,0,maxOverTimeValue); | |||
|  | 			} | |||
|  | 			else{ | |||
|  | 				axisValue = Mathf.Clamp( axisValue,-maxOverTimeValue,0); | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Axis value | |||
|  | 		ComputAxisValue(realValue, type,isOnDrag,deltaTime ); | |||
|  | 
 | |||
|  | 	} | |||
|  | 
 | |||
|  | 	public void UpdateButton(){ | |||
|  | 
 | |||
|  | 		// Auto link | |||
|  | 		if (autoLinkTagPlayer && player==null || ( player && !player.activeSelf)){ | |||
|  | 			player = GameObject.FindGameObjectWithTag(autoTag); | |||
|  | 			if (player){ | |||
|  | 				directTransform = player.transform; | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 		if (isValueOverTime){ | |||
|  | 			axisValue += overTimeStep * Time.deltaTime; | |||
|  | 			axisValue = Mathf.Clamp( axisValue,0,maxOverTimeValue); | |||
|  | 		} | |||
|  | 		else{ | |||
|  | 			if (axisState == AxisState.Press || axisState == AxisState.Down){ | |||
|  | 				axisValue = 1; | |||
|  | 			} | |||
|  | 			else{ | |||
|  | 				axisValue = 0; | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 		switch (actionOn){ | |||
|  | 			case ActionOn.Down: | |||
|  | 				axisSpeedValue = axisValue * speed ; | |||
|  | 				if (axisState == AxisState.Down){ | |||
|  | 					DoDirectAction(); | |||
|  | 				} | |||
|  | 				break; | |||
|  | 			case ActionOn.Press: | |||
|  | 				axisSpeedValue = axisValue * speed * Time.deltaTime; | |||
|  | 				if (axisState == AxisState.Press){ | |||
|  | 					DoDirectAction(); | |||
|  | 				} | |||
|  | 				break; | |||
|  | 			} | |||
|  | 	} | |||
|  | 
 | |||
|  | 	public void ResetAxis(){ | |||
|  | 		if (!isEnertia || (isEnertia && Mathf.Abs(axisValue)<inertiaThreshold)  ){ | |||
|  | 			axisValue =0; | |||
|  | 			axisSpeedValue =0; | |||
|  | 		} | |||
|  | 	} | |||
|  | 	 | |||
|  | 	public void DoDirectAction(){ | |||
|  | 	 | |||
|  | 		if (directTransform){ | |||
|  | 			Vector3 localAxis = GetInfluencedAxis(); | |||
|  | 
 | |||
|  | 			switch (  directAction){ | |||
|  | 			case ETCAxis.DirectAction.Rotate: | |||
|  | 				directTransform.Rotate( localAxis *  axisSpeedValue, Space.World); | |||
|  | 				break; | |||
|  | 				 | |||
|  | 			case ETCAxis.DirectAction.RotateLocal: | |||
|  | 				directTransform.Rotate( localAxis *  axisSpeedValue,Space.Self); | |||
|  | 				break; | |||
|  | 				 | |||
|  | 				 | |||
|  | 			case ETCAxis.DirectAction.Translate: | |||
|  | 				if ( directCharacterController==null){ | |||
|  | 
 | |||
|  | 					directTransform.Translate(localAxis *  axisSpeedValue,Space.World); | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					if (directCharacterController.isGrounded || !isLockinJump){ | |||
|  | 						Vector3 direction = localAxis *  axisSpeedValue; | |||
|  | 						directCharacterController.Move( direction  ); | |||
|  | 						lastMove = localAxis *  (axisSpeedValue/Time.deltaTime); | |||
|  | 					} | |||
|  | 					else{ | |||
|  | 						directCharacterController.Move( lastMove * Time.deltaTime); | |||
|  | 					} | |||
|  | 				} | |||
|  | 				break; | |||
|  | 				 | |||
|  | 				 | |||
|  | 			case ETCAxis.DirectAction.TranslateLocal: | |||
|  | 				if ( directCharacterController==null){ | |||
|  | 					directTransform.Translate(localAxis *  axisSpeedValue,Space.Self); | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					if (directCharacterController.isGrounded || !isLockinJump){ | |||
|  | 						Vector3 direction =  directCharacterController.transform.TransformDirection(localAxis) *  axisSpeedValue; | |||
|  | 						directCharacterController.Move( direction ); | |||
|  | 						lastMove =directCharacterController.transform.TransformDirection(localAxis) *  (axisSpeedValue/Time.deltaTime); | |||
|  | 					} | |||
|  | 					else{ | |||
|  | 						directCharacterController.Move( lastMove * Time.deltaTime ); | |||
|  | 					} | |||
|  | 				} | |||
|  | 				break;	 | |||
|  | 				 | |||
|  | 			case ETCAxis.DirectAction.Scale: | |||
|  | 				directTransform.localScale +=  localAxis *  axisSpeedValue; | |||
|  | 				break; | |||
|  | 
 | |||
|  | 			case ETCAxis.DirectAction.Force: | |||
|  | 				if (directRigidBody!=null){ | |||
|  | 					directRigidBody.AddForce( localAxis * axisValue * speed); | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					Debug.LogWarning("ETCAxis : "+ name + " No rigidbody on gameobject : "+ _directTransform.name);  | |||
|  | 				} | |||
|  | 				break; | |||
|  | 
 | |||
|  | 			case ETCAxis.DirectAction.RelativeForce: | |||
|  | 				if (directRigidBody!=null){ | |||
|  | 					directRigidBody.AddRelativeForce( localAxis * axisValue * speed); | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					Debug.LogWarning("ETCAxis : "+ name + " No rigidbody on gameobject : "+ _directTransform.name);  | |||
|  | 				} | |||
|  | 				break; | |||
|  | 
 | |||
|  | 			case ETCAxis.DirectAction.Torque: | |||
|  | 				if (directRigidBody!=null){ | |||
|  | 					directRigidBody.AddTorque(localAxis * axisValue * speed); | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					Debug.LogWarning("ETCAxis : "+ name + " No rigidbody on gameobject : "+ _directTransform.name);  | |||
|  | 				} | |||
|  | 				break; | |||
|  | 
 | |||
|  | 			case ETCAxis.DirectAction.RelativeTorque: | |||
|  | 				if (directRigidBody!=null){ | |||
|  | 					directRigidBody.AddRelativeTorque(localAxis * axisValue * speed); | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					Debug.LogWarning("ETCAxis : "+ name + " No rigidbody on gameobject : "+ _directTransform.name);  | |||
|  | 				} | |||
|  | 				break; | |||
|  | 
 | |||
|  | 			case ETCAxis.DirectAction.Jump: | |||
|  | 				if ( directCharacterController!=null){ | |||
|  | 
 | |||
|  | 					if (!isJump){ | |||
|  | 						isJump = true; | |||
|  | 						currentGravity = speed; | |||
|  | 					} | |||
|  | 				} | |||
|  | 				break; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			if (isClampRotation &&  directAction == DirectAction.RotateLocal){ | |||
|  | 				DoAngleLimitation(); | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 	} | |||
|  | 	 | |||
|  | 	public void DoGravity(){ | |||
|  | 
 | |||
|  | 		if (directCharacterController != null && gravity!=0){ | |||
|  | 		 | |||
|  | 			if (!isJump){ | |||
|  | 				Vector3 move = new Vector3(0,-gravity,0); | |||
|  | 				directCharacterController.Move( move * Time.deltaTime); | |||
|  | 			} | |||
|  | 			else{ | |||
|  | 				currentGravity -= gravity*Time.deltaTime; | |||
|  | 				Vector3 move = new Vector3(0,currentGravity,0); | |||
|  | 				directCharacterController.Move( move * Time.deltaTime); | |||
|  | 			} | |||
|  | 
 | |||
|  | 			if (directCharacterController.isGrounded){ | |||
|  | 				isJump = false; | |||
|  | 				currentGravity =0; | |||
|  | 			} | |||
|  | 
 | |||
|  | 
 | |||
|  | 		} | |||
|  | 	} | |||
|  | 	#endregion | |||
|  | 
 | |||
|  | 	#region Private methods | |||
|  | 
 | |||
|  | 	private void ComputAxisValue(float realValue, ETCBase.ControlType type, bool isOnDrag, bool deltaTime){ | |||
|  | 
 | |||
|  | 		if (enable){ | |||
|  | 
 | |||
|  | 			if (type == ETCBase.ControlType.Joystick){ | |||
|  | 
 | |||
|  | 				if (valueMethod == AxisValueMethod.Classical){ | |||
|  | 					float dist = Mathf.Max(Mathf.Abs(realValue),0.001f); | |||
|  | 					float dead = Mathf.Max(dist - deadValue, 0)/(1f - deadValue)/dist; | |||
|  | 					realValue *= dead; | |||
|  | 
 | |||
|  | 				} | |||
|  | 				else{ | |||
|  | 					//realValue = deadCurve.Evaluate( Mathf.Abs(realValue)) * Mathf.Sign( realValue); | |||
|  | 					realValue = curveValue.Evaluate( realValue); | |||
|  | 				} | |||
|  | 			} | |||
|  | 
 | |||
|  | 			if (isEnertia){ | |||
|  | 				realValue = (realValue-axisValue); | |||
|  | 				realValue /= inertia; | |||
|  | 
 | |||
|  | 				axisValue += realValue; | |||
|  | 
 | |||
|  | 				if (Mathf.Abs(axisValue)< inertiaThreshold && !isOnDrag ) { | |||
|  | 					axisValue = 0; | |||
|  | 				} | |||
|  | 			} | |||
|  | 			else if (!isValueOverTime || (isValueOverTime && realValue ==0)){ | |||
|  | 				axisValue = realValue; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			if (deltaTime){ | |||
|  | 				axisSpeedValue = axisValue * speed * Time.deltaTime; | |||
|  | 			} | |||
|  | 			else{ | |||
|  | 				axisSpeedValue = axisValue * speed; | |||
|  | 			} | |||
|  | 		} | |||
|  | 		else{ | |||
|  | 			axisValue = 0; | |||
|  | 			axisSpeedValue =0; | |||
|  | 		} | |||
|  | 	} | |||
|  | 		 | |||
|  | 	private Vector3 GetInfluencedAxis(){ | |||
|  | 		 | |||
|  | 		Vector3 axis = Vector3.zero; | |||
|  | 		 | |||
|  | 		switch(axisInfluenced){ | |||
|  | 		case ETCAxis.AxisInfluenced.X: | |||
|  | 			axis = Vector3.right; | |||
|  | 			break; | |||
|  | 		case ETCAxis.AxisInfluenced.Y: | |||
|  | 			axis = Vector3.up; | |||
|  | 			break; | |||
|  | 		case ETCAxis.AxisInfluenced.Z: | |||
|  | 			axis = Vector3.forward; | |||
|  | 			break; | |||
|  | 		}	 | |||
|  | 		 | |||
|  | 		return axis; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	private float GetAngle(){ | |||
|  | 		 | |||
|  | 		float angle=0; | |||
|  | 		 | |||
|  | 		if (_directTransform!=null){ | |||
|  | 			switch(axisInfluenced){ | |||
|  | 				case AxisInfluenced.X: | |||
|  | 					angle = _directTransform.localRotation.eulerAngles.x; | |||
|  | 					break; | |||
|  | 				case AxisInfluenced.Y: | |||
|  | 					angle = _directTransform.localRotation.eulerAngles.y; | |||
|  | 					break; | |||
|  | 				case AxisInfluenced.Z: | |||
|  | 					angle = _directTransform.localRotation.eulerAngles.z; | |||
|  | 					break;			 | |||
|  | 			}	 | |||
|  | 			 | |||
|  | 			if (angle<=360 && angle>=180){ | |||
|  | 				angle = angle -360;	 | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return angle; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	private void DoAutoStabilisation(){ | |||
|  | 		 | |||
|  | 		float angle= GetAngle(); | |||
|  | 
 | |||
|  | 		if (angle<=360 && angle>=180){ | |||
|  | 			angle = angle -360;	 | |||
|  | 		}		 | |||
|  | 
 | |||
|  | 		if (angle > startAngle - autoStabThreshold || angle < startAngle + autoStabThreshold){ | |||
|  | 			 | |||
|  | 			float axis=0; | |||
|  | 			Vector3 stabAngle = Vector3.zero; | |||
|  | 			 | |||
|  | 			if (angle > startAngle - autoStabThreshold){ | |||
|  | 				axis = angle + autoStabSpeed/100f*Mathf.Abs (angle-startAngle) * Time.deltaTime*-1; | |||
|  | 			} | |||
|  | 			 | |||
|  | 			 | |||
|  | 			if (angle < startAngle + autoStabThreshold){ | |||
|  | 				axis = angle + autoStabSpeed/100f*Mathf.Abs (angle-startAngle) * Time.deltaTime; | |||
|  | 			} | |||
|  | 			 | |||
|  | 			switch(axisInfluenced){ | |||
|  | 			case AxisInfluenced.X:	 | |||
|  | 				stabAngle = new Vector3(axis,_directTransform.localRotation.eulerAngles.y,_directTransform.localRotation.eulerAngles.z); | |||
|  | 				break; | |||
|  | 			case AxisInfluenced.Y:	 | |||
|  | 				stabAngle = new Vector3(_directTransform.localRotation.eulerAngles.x,axis,_directTransform.localRotation.eulerAngles.z); | |||
|  | 				break; | |||
|  | 			case AxisInfluenced.Z:	 | |||
|  | 				stabAngle = new Vector3(_directTransform.localRotation.eulerAngles.x,_directTransform.localRotation.eulerAngles.y,axis); | |||
|  | 				break; | |||
|  | 			} | |||
|  | 			 | |||
|  | 			_directTransform.localRotation  = Quaternion.Euler( stabAngle);	 | |||
|  | 		} | |||
|  | 	} | |||
|  | 
 | |||
|  | 	private void DoAngleLimitation(){ | |||
|  | 
 | |||
|  | 		Quaternion q = _directTransform.localRotation; | |||
|  | 		 | |||
|  | 		q.x /= q.w; | |||
|  | 		q.y /= q.w; | |||
|  | 		q.z /= q.w; | |||
|  | 		q.w = 1.0f; | |||
|  | 		 | |||
|  | 		float newAngle = 0; | |||
|  | 		 | |||
|  | 		switch(axisInfluenced){ | |||
|  | 		case AxisInfluenced.X: | |||
|  | 			newAngle = 2.0f * Mathf.Rad2Deg * Mathf.Atan (q.x); | |||
|  | 			newAngle = Mathf.Clamp (newAngle, -minAngle, maxAngle); | |||
|  | 			q.x = Mathf.Tan (0.5f * Mathf.Deg2Rad * newAngle); | |||
|  | 			break; | |||
|  | 		case AxisInfluenced.Y: | |||
|  | 			newAngle = 2.0f * Mathf.Rad2Deg * Mathf.Atan (q.y); | |||
|  | 			newAngle = Mathf.Clamp (newAngle, -minAngle, maxAngle); | |||
|  | 			q.y = Mathf.Tan (0.5f * Mathf.Deg2Rad * newAngle); | |||
|  | 			break; | |||
|  | 		case AxisInfluenced.Z: | |||
|  | 			newAngle = 2.0f * Mathf.Rad2Deg * Mathf.Atan (q.z); | |||
|  | 			newAngle = Mathf.Clamp (newAngle, -minAngle, maxAngle); | |||
|  | 			q.z = Mathf.Tan (0.5f * Mathf.Deg2Rad * newAngle); | |||
|  | 			break; | |||
|  | 		} | |||
|  | 		 | |||
|  | 		 | |||
|  | 		 | |||
|  | 		_directTransform.localRotation = q; | |||
|  | 		 | |||
|  | 	} | |||
|  | 	#endregion | |||
|  | 
 | |||
|  | 	public void InitDeadCurve(){ | |||
|  | 
 | |||
|  | 		curveValue = AnimationCurve.EaseInOut(0,0,1,1); | |||
|  | 		curveValue.postWrapMode = WrapMode.PingPong; | |||
|  | 		curveValue.preWrapMode = WrapMode.PingPong; | |||
|  | 	} | |||
|  | 
 | |||
|  | 
 | |||
|  | } |