commit 05fa94d999420a8e6f27a8a1a1eed37e6795e1d7
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Thu Jan 23 00:16:12 2020 +0000

    RubikCube: detect if the cube is solved.
    
    In order to do this correctly, we also needed to keep correcting each mQuatScramble quaternions after each quatMultiplication in order to avoid multiplication errors accumulating. This turns out to be easy, because each quaternion representing a legal combination of rotations of a RubikCube must have each of the 4 to its components be equal to one of only 7 possible floats.

diff --git a/src/main/java/org/distorted/magic/RubikCube.java b/src/main/java/org/distorted/magic/RubikCube.java
index b02bed62..07bfbaed 100644
--- a/src/main/java/org/distorted/magic/RubikCube.java
+++ b/src/main/java/org/distorted/magic/RubikCube.java
@@ -53,6 +53,9 @@ public class RubikCube extends DistortedNode
     private static final Static3D VectY = new Static3D(0,1,0);
     private static final Static3D VectZ = new Static3D(0,0,1);
 
+    private static final float SQ2 = 0.5f*((float)Math.sqrt(2));
+    private static final float[] LEGAL = { 0.0f , 0.5f , -0.5f , 1.0f , -1.0f , SQ2 , -SQ2 };
+
     public static final int VECTX = 0;  //
     public static final int VECTY = 1;  // don't change this
     public static final int VECTZ = 2;  //
@@ -185,6 +188,35 @@ public class RubikCube extends DistortedNode
             }
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    boolean isSolved()
+      {
+      Static4D q = mQuatScramble[0][0][0];
+
+      float x = q.get1();
+      float y = q.get2();
+      float z = q.get3();
+      float w = q.get4();
+
+      for(int i = 0; i< mSize; i++)
+        for(int j = 0; j< mSize; j++)
+          for(int k = 0; k< mSize; k++)
+            {
+            if( i==0 || i==mSize-1 || j==0 || j==mSize-1 || k==0 || k==mSize-1 )
+              {
+              q = mQuatScramble[i][j][k];
+
+              if( q.get1()!=x || q.get2()!=y || q.get3()!=z || q.get4()!=w )
+                {
+                return false;
+                }
+              }
+            }
+
+      return true;
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void apply(Effect effect, int position)
@@ -498,6 +530,7 @@ public class RubikCube extends DistortedNode
 
                 mRotationAngle[x][y][z].removeAll();
                 mQuatScramble[x][y][z].set(RubikSurfaceView.quatMultiply(quat,mQuatScramble[x][y][z]));
+                normalizeScrambleQuat(x,y,z);
                 modifyCurrentPosition(x,y,z,quat);
                 }
               }
@@ -505,6 +538,73 @@ public class RubikCube extends DistortedNode
       mRotationAngleStatic.set1(0);
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// All legal rotation quats must have all four of their components equal to either
+// 0, 1, -1, 0.5, -0.5 or +-sqrt(2)/2.
+//
+// Because of quatMultiplication, errors can accumulate - so to avoid this, we
+// correct the value of the 'scramble' quat to what it should be.
+//
+// We also have to remember that the group of unit quaternions is a double-cover of rotations
+// in 3D ( q represents the same rotation as -q ) - so invert if needed.
+
+    private void normalizeScrambleQuat(int i, int j, int k)
+      {
+      Static4D quat = mQuatScramble[i][j][k];
+
+      float x = quat.get1();
+      float y = quat.get2();
+      float z = quat.get3();
+      float w = quat.get4();
+      float diff;
+
+      for(int legal=0; legal<LEGAL.length; legal++)
+        {
+        diff = x-LEGAL[legal];
+        if( diff*diff<0.01f ) x = LEGAL[legal];
+        diff = y-LEGAL[legal];
+        if( diff*diff<0.01f ) y = LEGAL[legal];
+        diff = z-LEGAL[legal];
+        if( diff*diff<0.01f ) z = LEGAL[legal];
+        diff = w-LEGAL[legal];
+        if( diff*diff<0.01f ) w = LEGAL[legal];
+        }
+
+      if( w<0 )
+        {
+        w = -w;
+        z = -z;
+        y = -y;
+        x = -x;
+        }
+      else if( w==0 )
+        {
+        if( z<0 )
+          {
+          z = -z;
+          y = -y;
+          x = -x;
+          }
+        else if( z==0 )
+          {
+          if( y<0 )
+            {
+            y = -y;
+            x = -x;
+            }
+          else if( y==0 )
+            {
+            if( x<0 )
+              {
+              x = -x;
+              }
+            }
+          }
+        }
+
+      mQuatScramble[i][j][k].set(x,y,z,w);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     private float getSinkStrength()
diff --git a/src/main/java/org/distorted/magic/RubikRenderer.java b/src/main/java/org/distorted/magic/RubikRenderer.java
index 110efcde..bd42b2bf 100644
--- a/src/main/java/org/distorted/magic/RubikRenderer.java
+++ b/src/main/java/org/distorted/magic/RubikRenderer.java
@@ -47,7 +47,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
     private int mNextCubeSize, mScrambleCubeNum;
     private long mRotationFinishedID;
     private long[] mEffectID;
-    private boolean mFinishRotation, mRemoveRotation, mFinishDragCurrent, mFinishDragAccumulated;
+    private boolean mFinishRotation, mRemoveRotation, mSetQuatCurrent, mSetQuatAccumulated;
     private boolean mSizeChangeCube, mSolveCube, mScrambleCube;
     private boolean mCanRotate, mCanDrag, mCanUI;
     private RubikCube mOldCube, mNewCube;
@@ -68,13 +68,13 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
       mScreenWidth = mScreenHeight = 0;
       mScrambleCubeNum = 0;
 
-      mFinishRotation        = false;
-      mRemoveRotation        = false;
-      mFinishDragCurrent     = false;
-      mFinishDragAccumulated = false;
-      mSizeChangeCube        = false;
-      mSolveCube             = false;
-      mScrambleCube          = false;
+      mFinishRotation     = false;
+      mRemoveRotation     = false;
+      mSetQuatCurrent     = false;
+      mSetQuatAccumulated = false;
+      mSizeChangeCube     = true;
+      mSolveCube          = false;
+      mScrambleCube       = false;
 
       mCanRotate   = true;
       mCanDrag     = true;
@@ -83,7 +83,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
       mEffectID = new long[BaseEffect.Type.LENGTH];
 
       mMesh= new MeshFlat(20,20);
-      mNextCubeSize =RubikActivity.getSize();
+      mNextCubeSize = RubikActivity.getSize();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -95,15 +95,15 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
       {
       mScreen.render( System.currentTimeMillis() );
 
-      if( mFinishDragCurrent )
+      if( mSetQuatCurrent )
         {
-        mFinishDragCurrent = false;
+        mSetQuatCurrent = false;
         mView.setQuatCurrent();
         }
 
-      if( mFinishDragAccumulated )
+      if( mSetQuatAccumulated )
         {
-        mFinishDragAccumulated = false;
+        mSetQuatAccumulated = false;
         mView.setQuatAccumulated();
         }
 
@@ -118,6 +118,12 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
         {
         mRemoveRotation=false;
         mNewCube.removeRotationNow();
+
+        if( mNewCube.isSolved() )
+          {
+          android.util.Log.e("renderer", "CUBE IS SOLVED NOW");
+          }
+
         mCanRotate = true;
         }
 
@@ -127,7 +133,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
         mCanDrag        = false;
         mCanRotate      = false;
         createCubeNow(mNextCubeSize);
-        doEffectNow(0);
+        doEffectNow( BaseEffect.Type.SIZECHANGE );
         }
 
       if( mSolveCube )
@@ -136,7 +142,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
         mCanDrag     = false;
         mCanRotate   = false;
         mCanUI       = false;
-        doEffectNow(1);
+        doEffectNow( BaseEffect.Type.SOLVE );
         }
 
       if( mScrambleCube )
@@ -145,7 +151,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
         mCanDrag      = false;
         mCanRotate    = false;
         mCanUI        = false;
-        doEffectNow(2);
+        doEffectNow( BaseEffect.Type.SCRAMBLE );
         }
       }
 
@@ -296,9 +302,11 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // do all 'adjustable' effects (SizeChange, Solve, Scramble)
 
-   private void doEffectNow(int index)
+   private void doEffectNow(BaseEffect.Type type)
      {
-     mEffectID[index] = BaseEffect.Type.getType(index).startEffect(this);
+     int index = type.ordinal();
+
+     mEffectID[index] = type.startEffect(this);
 
      if( mEffectID[index] == -1 )
        {
@@ -361,13 +369,13 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
 
    void setQuatCurrentOnNextRender()
      {
-     mFinishDragCurrent = true;
+     mSetQuatCurrent = true;
      }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
    void setQuatAccumulatedOnNextRender()
      {
-     mFinishDragAccumulated = true;
+     mSetQuatAccumulated = true;
      }
 }
