commit 0e5ad27cfe73978fdd2a46f97dfe3c92b528c358
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Sat Apr 11 00:23:08 2020 +0100

    Add a 'withdraw move' button to the Solving UI state.

diff --git a/src/main/java/org/distorted/main/RubikSurfaceView.java b/src/main/java/org/distorted/main/RubikSurfaceView.java
index a0b0cf94..37e3b564 100644
--- a/src/main/java/org/distorted/main/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/main/RubikSurfaceView.java
@@ -74,6 +74,8 @@ public class RubikSurfaceView extends GLSurfaceView
     private float mAxisX, mAxisY;
     private float mRotationFactor;
     private int mLastCubitColor, mLastCubitFace, mLastCubit;
+    private int mCurrentAxis, mCurrentRow;
+    private float mCurrentAngle;
 
     private static Static4D mQuatCurrent    = new Static4D(0,0,0,1);
     private static Static4D mQuatAccumulated= new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
@@ -363,12 +365,13 @@ public class RubikSurfaceView extends GLSurfaceView
                                            Static2D res = mMovement.newRotation(rotatedTouchPoint2);
                                            RubikObject object = mPostRender.getObject();
 
-                                           int axis = (int)res.get0();
+                                           mCurrentAxis = (int)res.get0();
                                            float offset = res.get1();
-                                           computeCurrentAxis( object.getRotationAxis()[axis] );
+                                           mCurrentRow = (int)(object.returnMultiplier()*offset);
+                                           computeCurrentAxis( object.getRotationAxis()[mCurrentAxis] );
                                            mRotationFactor = object.returnRotationFactor(offset);
 
-                                           object.beginNewRotation( axis, (int)(object.returnMultiplier()*offset) );
+                                           object.beginNewRotation( mCurrentAxis, mCurrentRow );
 
                                            if( RubikState.getCurrentState()==RubikState.SOLV )
                                              {
@@ -383,7 +386,8 @@ public class RubikSurfaceView extends GLSurfaceView
                                        else if( mContinuingRotation )
                                          {
                                          float angle = continueRotation(x-mStartRotX,y-mStartRotY);
-                                         mPostRender.getObject().continueRotation(SWIPING_SENSITIVITY*angle);
+                                         mCurrentAngle = SWIPING_SENSITIVITY*angle;
+                                         mPostRender.getObject().continueRotation(mCurrentAngle);
                                          }
                                        else if( mDragging )
                                          {
@@ -416,6 +420,16 @@ public class RubikSurfaceView extends GLSurfaceView
                                        if( mContinuingRotation )
                                          {
                                          mPostRender.finishRotation();
+
+                                         if( RubikState.getCurrentState()==RubikState.SOLV )
+                                           {
+                                           RubikStateSolving solving = (RubikStateSolving)RubikState.SOLV.getStateClass();
+
+                                           int angle = mPostRender.getObject().computeNearestAngle(mCurrentAngle);
+
+                                           if( angle!=0 )
+                                             solving.addMove(mCurrentAxis, mCurrentRow, angle);
+                                           }
                                          }
                                        if( mLastCubitColor>=0 )
                                          {
diff --git a/src/main/java/org/distorted/objects/Cubit.java b/src/main/java/org/distorted/objects/Cubit.java
index e1cd4bd9..ff43e5f9 100644
--- a/src/main/java/org/distorted/objects/Cubit.java
+++ b/src/main/java/org/distorted/objects/Cubit.java
@@ -120,18 +120,6 @@ class Cubit
     quat.set(x,y,z,w);
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int computeNearestAngle(float angle)
-    {
-    final int NEAREST = 360/mParent.getBasicAngle();
-
-    int tmp = (int)((angle+NEAREST/2)/NEAREST);
-    if( angle< -(NEAREST*0.5) ) tmp-=1;
-
-    return NEAREST*tmp;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void modifyCurrentPosition(Static4D quat)
@@ -317,7 +305,7 @@ class Cubit
     if( pointNum>=1 )
       {
       float startingAngle = mRotationAngle.getPoint(pointNum-1).get0();
-      int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+      int nearestAngleInDegrees = mParent.computeNearestAngle(startingAngle);
       mParent.mRotationAngleStatic.set0(startingAngle);
       mParent.mRotationAngleFinal.set0(nearestAngleInDegrees);
       mParent.mRotationAngleMiddle.set0( nearestAngleInDegrees + (nearestAngleInDegrees-startingAngle)*0.2f );
@@ -340,7 +328,7 @@ class Cubit
       float axisZ = mParent.ROTATION_AXIS[axis].get2();
 
       float startingAngle = mRotationAngle.getPoint(pointNum-1).get0();
-      int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+      int nearestAngleInDegrees = mParent.computeNearestAngle(startingAngle);
       double nearestAngleInRadians = nearestAngleInDegrees*Math.PI/180;
       float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
       float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
diff --git a/src/main/java/org/distorted/objects/RubikObject.java b/src/main/java/org/distorted/objects/RubikObject.java
index a1d8a465..87180f14 100644
--- a/src/main/java/org/distorted/objects/RubikObject.java
+++ b/src/main/java/org/distorted/objects/RubikObject.java
@@ -567,6 +567,18 @@ public abstract class RubikObject extends DistortedNode
     return currentBest;
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeNearestAngle(float angle)
+    {
+    final int NEAREST = 360/getBasicAngle();
+
+    int tmp = (int)((angle+NEAREST/2)/NEAREST);
+    if( angle< -(NEAREST*0.5) ) tmp-=1;
+
+    return NEAREST*tmp;
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public RubikObjectList getObjectList()
diff --git a/src/main/java/org/distorted/states/RubikStateSolving.java b/src/main/java/org/distorted/states/RubikStateSolving.java
index dad555ed..0f019954 100644
--- a/src/main/java/org/distorted/states/RubikStateSolving.java
+++ b/src/main/java/org/distorted/states/RubikStateSolving.java
@@ -24,27 +24,48 @@ import android.util.DisplayMetrics;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
+import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
+import org.distorted.main.RubikPostRender;
+import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
 import org.distorted.scores.RubikScores;
 
+import java.util.ArrayList;
 import java.util.Timer;
 import java.util.TimerTask;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class RubikStateSolving extends RubikStateAbstract
+public class RubikStateSolving extends RubikStateAbstract implements RubikPostRender.ActionFinishedListener
   {
+  private static final int DURATION_MILLIS = 750;
+
   private TextView mTime;
   private Timer mTimer;
   private long mStartTime;
   private boolean mRunning;
   private RubikScores mScores;
   private Button mBack;
+  private ImageButton mPrevButton;
+  private boolean mCanPrevMove;
+  private ArrayList<Move> mMoves;
+
+  private static class Move
+    {
+    private int mAxis, mRow, mAngle;
+
+    Move(int axis, int row, int angle)
+      {
+      mAxis = axis;
+      mRow  = row;
+      mAngle= angle;
+      }
+    }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -64,7 +85,14 @@ public class RubikStateSolving extends RubikStateAbstract
 
   void enterState(final RubikActivity act)
     {
+    mCanPrevMove = true;
+
+    if( mMoves==null ) mMoves = new ArrayList<>();
+    else               mMoves.clear();
+
     LayoutInflater inflater = act.getLayoutInflater();
+    DisplayMetrics metrics = act.getResources().getDisplayMetrics();
+    float scale = metrics.density;
 
     // TOP ////////////////////////////
     LinearLayout layoutTop = act.findViewById(R.id.upperBar);
@@ -76,11 +104,13 @@ public class RubikStateSolving extends RubikStateAbstract
     // BOT ////////////////////////////
     LinearLayout layoutLeft = act.findViewById(R.id.mainBarLeft);
     layoutLeft.removeAllViews();
+
+    if( mPrevButton==null ) setupPrevMoveButtom(act,scale);
+    layoutLeft.addView(mPrevButton);
+
     LinearLayout layoutRight = act.findViewById(R.id.mainBarRight);
     layoutRight.removeAllViews();
 
-    DisplayMetrics metrics = act.getResources().getDisplayMetrics();
-    float scale = metrics.density;
     int padding = (int)(5*scale + 0.5f);
     LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
 
@@ -101,6 +131,82 @@ public class RubikStateSolving extends RubikStateAbstract
     layoutRight.addView(mBack);
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevMoveButtom(final RubikActivity act, float scale)
+    {
+    int padding = (int)( 3*scale + 0.5f);
+    int width   = (int)(60*scale + 0.5f);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width,LinearLayout.LayoutParams.MATCH_PARENT);
+    mPrevButton = new ImageButton(act);
+    mPrevButton.setLayoutParams(params);
+    mPrevButton.setPadding(padding,0,padding,0);
+    mPrevButton.setImageResource(R.drawable.left);
+    mPrevButton.setEnabled(false);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        RubikPostRender post = act.getPostRender();
+        backMove(post);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void backMove(RubikPostRender post)
+    {
+    if( mCanPrevMove )
+      {
+      int numMoves = mMoves.size();
+
+      if( numMoves>0 )
+        {
+        Move move = mMoves.remove(numMoves-1);
+        RubikObject object = post.getObject();
+
+        int axis  = move.mAxis;
+        int row   = (1<<move.mRow);
+        int angle = move.mAngle;
+        int numRot= Math.abs(angle*object.getBasicAngle()/360);
+
+        if( angle!=0 )
+          {
+          mCanPrevMove = false;
+          post.addRotation(this, axis, row, -angle, numRot*DURATION_MILLIS);
+
+          if( numMoves==1 )
+            {
+            mPrevButton.setEnabled(false);
+            }
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: trying to back move of angle 0");
+          }
+        }
+      else
+        {
+        android.util.Log.e("solv", "error: no moves to back!");
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addMove(int axis, int row, int angle)
+    {
+    mMoves.add(new Move(axis,row,angle));
+
+    if( mMoves.size()==1 )
+      {
+      mPrevButton.setEnabled(true);
+      }
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void savePreferences(SharedPreferences.Editor editor)
@@ -182,4 +288,11 @@ public class RubikStateSolving extends RubikStateAbstract
 
     return 0;
     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onActionFinished(final long effectID)
+    {
+    mCanPrevMove = true;
+    }
   }
