commit 8d50e08da634456df35816b2f304f60f2b9fdb87
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Thu Jun 18 22:03:11 2020 +0100

    Initial attempt at two-finger rotation.
    Mostly works; still, one corner case doesnt:
    
    - put two fingers down, start rotating
    - lift one of them up
    - put it back down
    
    depending if we lifted the first or second finger, weird things may happen.

diff --git a/src/main/java/org/distorted/main/RubikSurfaceView.java b/src/main/java/org/distorted/main/RubikSurfaceView.java
index 1d86a022..4e808346 100644
--- a/src/main/java/org/distorted/main/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/main/RubikSurfaceView.java
@@ -42,6 +42,7 @@ import org.distorted.states.RubikStateSolving;
 public class RubikSurfaceView extends GLSurfaceView
 {
     private static final int NUM_SPEED_PROBES = 10;
+    private static final int INVALID_POINTER_ID = -1;
 
     public static final int MODE_ROTATE  = 0;
     public static final int MODE_DRAG    = 1;
@@ -72,7 +73,8 @@ public class RubikSurfaceView extends GLSurfaceView
     private boolean mDragging, mBeginningRotation, mContinuingRotation;
     private int mScreenWidth, mScreenHeight, mScreenMin;
 
-    private float mX, mY;
+    private int mPtrID1, mPtrID2;
+    private float mX, mY, mBegX1, mBegY1, mBegX2, mBegY2;
     private float mStartRotX, mStartRotY;
     private float mAxisX, mAxisY;
     private float mRotationFactor;
@@ -156,6 +158,16 @@ public class RubikSurfaceView extends GLSurfaceView
       mMovement = movement;
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private Static4D quatFromAngle(float angle)
+      {
+      float cosA = (float)Math.cos(angle);
+      float sinA =-(float)Math.sin(angle);
+
+      return new Static4D(0, 0, sinA, cosA);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     private Static4D quatFromDrag(float dragX, float dragY)
@@ -383,94 +395,142 @@ public class RubikSurfaceView extends GLSurfaceView
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void actionDown(float x, float y)
+    private void dragging(float x, float y)
       {
-      mX = x;
-      mY = y;
-      setUpDragOrRotate(true,x,y);
+      mTempCurrent.set(quatFromDrag(mX-x,y-mY));
+      mPreRender.setQuatCurrentOnNextRender();
+
+      if( retFingerDragDistanceInInches(mX,mY,x,y) > DIRECTION_SENSITIVITY )
+        {
+        mX = x;
+        mY = y;
+        mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
+        mTempCurrent.set(0f, 0f, 0f, 1f);
+        mPreRender.setQuatCurrentOnNextRender();
+        mPreRender.setQuatAccumulatedOnNextRender();
+        }
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void actionMove(float x, float y)
+    private void continuingRotation(float x, float y)
       {
-      if( mBeginningRotation )
-        {
-        if( retFingerDragDistanceInInches(mX,mY,x,y) > ROTATION_SENSITIVITY )
-          {
-          mStartRotX = x;
-          mStartRotY = y;
-
-          Static4D touchPoint2 = new Static4D(x, y, 0, 0);
-          Static4D rotatedTouchPoint2= rotateVectorByInvertedQuat(touchPoint2, mQuatAccumulated);
+      float angle = continueRotation(x-mStartRotX,y-mStartRotY);
+      mCurrentAngle = SWIPING_SENSITIVITY*angle;
+      mPreRender.getObject().continueRotation(mCurrentAngle);
 
-          Static2D res = mMovement.newRotation(rotatedTouchPoint2);
-          RubikObject object = mPreRender.getObject();
+      addSpeedProbe(x,y);
+      }
 
-          mCurrentAxis = (int)res.get0();
-          float offset = res.get1();
-          mCurrentRow = (int)(object.returnMultiplier()*offset);
-          computeCurrentAxis( object.getRotationAxis()[mCurrentAxis] );
-          mRotationFactor = object.returnRotationFactor(offset);
+///////////////////////////////////////////////////////////////////////////////////////////////////
 
-          object.beginNewRotation( mCurrentAxis, mCurrentRow );
+    private void beginningRotation(float x, float y)
+      {
+      mStartRotX = x;
+      mStartRotY = y;
 
-          if( RubikState.getCurrentState()==RubikState.READ )
-            {
-            RubikStateSolving solving = (RubikStateSolving)RubikState.SOLV.getStateClass();
-            solving.resetElapsed();
+      Static4D touchPoint2 = new Static4D(x, y, 0, 0);
+      Static4D rotatedTouchPoint2= rotateVectorByInvertedQuat(touchPoint2, mQuatAccumulated);
 
-            final RubikActivity act = (RubikActivity)getContext();
+      Static2D res = mMovement.newRotation(rotatedTouchPoint2);
+      RubikObject object = mPreRender.getObject();
 
-            act.runOnUiThread(new Runnable()
-              {
-              @Override
-              public void run()
-                {
-                RubikState.switchState( act, RubikState.SOLV);
-                }
-              });
-            }
+      mCurrentAxis = (int)res.get0();
+      float offset = res.get1();
+      mCurrentRow = (int)(object.returnMultiplier()*offset);
+      computeCurrentAxis( object.getRotationAxis()[mCurrentAxis] );
+      mRotationFactor = object.returnRotationFactor(offset);
 
-          addSpeedProbe(x,y);
+      object.beginNewRotation( mCurrentAxis, mCurrentRow );
 
-          mBeginningRotation = false;
-          mContinuingRotation= true;
-          }
-        }
-      else if( mContinuingRotation )
+      if( RubikState.getCurrentState()==RubikState.READ )
         {
-        float angle = continueRotation(x-mStartRotX,y-mStartRotY);
-        mCurrentAngle = SWIPING_SENSITIVITY*angle;
-        mPreRender.getObject().continueRotation(mCurrentAngle);
+        RubikStateSolving solving = (RubikStateSolving)RubikState.SOLV.getStateClass();
+        solving.resetElapsed();
+
+        final RubikActivity act = (RubikActivity)getContext();
 
-        addSpeedProbe(x,y);
+        act.runOnUiThread(new Runnable()
+          {
+          @Override
+          public void run()
+            {
+            RubikState.switchState( act, RubikState.SOLV);
+            }
+          });
         }
-      else if( mDragging )
+
+      addSpeedProbe(x,y);
+
+      mBeginningRotation = false;
+      mContinuingRotation= true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionMove(MotionEvent event)
+      {
+      if( mPtrID2 == INVALID_POINTER_ID )
         {
-        mTempCurrent.set(quatFromDrag(mX-x,y-mY));
-        mPreRender.setQuatCurrentOnNextRender();
+        float x = (event.getX() - mScreenWidth*0.5f)/mScreenMin;
+        float y = (mScreenHeight*0.5f -event.getY())/mScreenMin;
 
-        if( retFingerDragDistanceInInches(mX,mY,x,y) > DIRECTION_SENSITIVITY )
+        if( mBeginningRotation )
           {
-          mX = x;
-          mY = y;
-          mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
-          mTempCurrent.set(0f, 0f, 0f, 1f);
-          mPreRender.setQuatCurrentOnNextRender();
-          mPreRender.setQuatAccumulatedOnNextRender();
+          if( retFingerDragDistanceInInches(mX,mY,x,y) > ROTATION_SENSITIVITY )
+            {
+            beginningRotation(x,y);
+            }
+          }
+        else if( mContinuingRotation )
+          {
+          continuingRotation(x,y);
+          }
+        else if( mDragging )
+          {
+          dragging(x,y);
+          }
+        else
+          {
+          setUpDragOrRotate(false,x,y);
           }
         }
       else
         {
-        setUpDragOrRotate(false,x,y);
+        int index1 = event.findPointerIndex(mPtrID1);
+        int index2 = event.findPointerIndex(mPtrID2);
+
+        float nX1 = event.getX(index1);
+        float nY1 = event.getY(index1);
+        float nX2 = event.getX(index2);
+        float nY2 = event.getY(index2);
+
+        float angle1 = (float) Math.atan2(mBegY1-mBegY2, mBegX1-mBegX2);
+        float angle2 = (float) Math.atan2( nY1-nY2     , nX1-nX2      );
+
+        mTempCurrent.set(quatFromAngle(angle1-angle2));
+        mPreRender.setQuatCurrentOnNextRender();
         }
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionDown(MotionEvent event)
+      {
+      mPtrID1 = event.getPointerId(0);
+
+      mX = (event.getX() - mScreenWidth*0.5f)/mScreenMin;
+      mY = (mScreenHeight*0.5f -event.getY())/mScreenMin;
+
+      setUpDragOrRotate(true,mX,mY);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     private void actionUp()
       {
+      mPtrID1 = INVALID_POINTER_ID;
+
       if( mDragging )
         {
         mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
@@ -498,6 +558,31 @@ public class RubikSurfaceView extends GLSurfaceView
         }
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionDown2(MotionEvent event)
+      {
+      mPtrID2 = event.getPointerId(event.getActionIndex());
+
+      int index1 = event.findPointerIndex(mPtrID1);
+      mBegX1 = event.getX(index1);
+      mBegY1 = event.getY(index1);
+
+      mX = (mBegX1 - mScreenWidth*0.5f)/mScreenMin;
+      mY = (mScreenHeight*0.5f -mBegY1)/mScreenMin;
+
+      int index2 = event.findPointerIndex(mPtrID2);
+      mBegX2 = event.getX(index2);
+      mBegY2 = event.getY(index2);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionUp2()
+      {
+      mPtrID2 = INVALID_POINTER_ID;
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -517,6 +602,9 @@ public class RubikSurfaceView extends GLSurfaceView
         mFirstIndex =0;
         mLastIndex  =0;
 
+        mPtrID1 = INVALID_POINTER_ID;
+        mPtrID2 = INVALID_POINTER_ID;
+
         mRenderer  = new RubikRenderer(this);
         mPreRender = new RubikPreRender(this);
 
@@ -542,15 +630,15 @@ public class RubikSurfaceView extends GLSurfaceView
     @Override
     public boolean onTouchEvent(MotionEvent event)
       {
-      int action = event.getAction();
-      float x = (event.getX() - mScreenWidth*0.5f)/mScreenMin;
-      float y = (mScreenHeight*0.5f -event.getY())/mScreenMin;
+      int action = event.getActionMasked();
 
       switch(action)
          {
-         case MotionEvent.ACTION_DOWN: actionDown(x,y); break;
-         case MotionEvent.ACTION_MOVE: actionMove(x,y); break;
-         case MotionEvent.ACTION_UP  : actionUp()     ; break;
+         case MotionEvent.ACTION_DOWN        : actionDown(event) ; break;
+         case MotionEvent.ACTION_MOVE        : actionMove(event) ; break;
+         case MotionEvent.ACTION_UP          : actionUp()        ; break;
+         case MotionEvent.ACTION_POINTER_DOWN: actionDown2(event); break;
+         case MotionEvent.ACTION_POINTER_UP  : actionUp2()       ; break;
          }
 
       return true;
