commit f16ff19d5aa6c2e157b871be0c4a1975a8e509a6
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Feb 21 13:53:54 2020 +0000

    Separate RubikCubit inner class.

diff --git a/src/main/java/org/distorted/object/RubikCube.java b/src/main/java/org/distorted/object/RubikCube.java
index 41c60655..78e9c9cf 100644
--- a/src/main/java/org/distorted/object/RubikCube.java
+++ b/src/main/java/org/distorted/object/RubikCube.java
@@ -52,123 +52,198 @@ class RubikCube extends RubikObject
     private static final Static3D VectY = new Static3D(0,1,0);
     private static final Static3D VectZ = new Static3D(0,0,1);
 
-    private DistortedNode[][][] mNodes;
-    private MeshCubes[][][] mCubes;
-    private DistortedEffects[][][] mEffects;
-    private Static4D[][][] mQuatScramble;
-    private Static3D[][][] mRotationAxis;
-    private Dynamic1D[][][] mRotationAngle;
-    private Static3D[][][] mCurrentPosition;
-    private MatrixEffectRotate[][][] mRotateEffect;
+    private static final Static3D matrCenter = new Static3D(0,0,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 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 };
-
-    private void normalizeScrambleQuat(int i, int j, int k)
+    private class Cubit
       {
-      Static4D quat = mQuatScramble[i][j][k];
+      private final Static3D mOrigPosition;
+
+      DistortedNode mNode;
+      MeshCubes mCube;
+      DistortedEffects mEffect;
+      Static4D mQuatScramble;
+      Static3D mRotationAxis;
+      Dynamic1D mRotationAngle;
+      Static3D mCurrentPosition;
 
-      float x = quat.get0();
-      float y = quat.get1();
-      float z = quat.get2();
-      float w = quat.get3();
-      float diff;
+      MatrixEffectRotate mRotateEffect;
 
-      for(float legal: LEGAL)
+    /////////////////////////////////////////////////////////////////////////////////
+
+      Cubit(MeshCubes mesh,Static3D vector, Static3D position)
         {
-        diff = x-legal;
-        if( diff*diff<0.01f ) x = legal;
-        diff = y-legal;
-        if( diff*diff<0.01f ) y = legal;
-        diff = z-legal;
-        if( diff*diff<0.01f ) z = legal;
-        diff = w-legal;
-        if( diff*diff<0.01f ) w = legal;
+        mOrigPosition = new Static3D( position.get0(), position.get1(), position.get2() );
+
+        mCube = mesh;
+        mQuatScramble    = new Static4D(0,0,0,1);
+        mRotationAngle   = new Dynamic1D();
+        mRotationAxis    = new Static3D(1,0,0);
+        mCurrentPosition = position;
+        mRotateEffect    = new MatrixEffectRotate(mRotationAngle, mRotationAxis, matrCenter);
+
+        mEffect = new DistortedEffects();
+        mEffect.apply(mSinkEffect);
+        mEffect.apply( new MatrixEffectMove(vector) );
+        mEffect.apply( new MatrixEffectQuaternion(mQuatScramble, matrCenter));
+        mEffect.apply(mRotateEffect);
+        mEffect.apply(mQuatAEffect);
+        mEffect.apply(mQuatCEffect);
+        mEffect.apply(mScaleEffect);
+        mEffect.apply(mMoveEffect);
+
+        mNode = new DistortedNode(mTexture,mEffect,mCube);
         }
 
-      if( w<0 )
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void savePreferences(SharedPreferences.Editor editor)
         {
-        w = -w;
-        z = -z;
-        y = -y;
-        x = -x;
+        String number = mOrigPosition.get0()+"_"+mOrigPosition.get1()+"_"+mOrigPosition.get2();
+
+        editor.putFloat("qx_"+number, mQuatScramble.get0());
+        editor.putFloat("qy_"+number, mQuatScramble.get1());
+        editor.putFloat("qz_"+number, mQuatScramble.get2());
+        editor.putFloat("qw_"+number, mQuatScramble.get3());
         }
-      else if( w==0 )
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void restorePreferences(SharedPreferences preferences)
         {
-        if( z<0 )
+        String number = mOrigPosition.get0()+"_"+mOrigPosition.get1()+"_"+mOrigPosition.get2();
+
+        float qx = preferences.getFloat("qx_"+number, 0.0f);
+        float qy = preferences.getFloat("qy_"+number, 0.0f);
+        float qz = preferences.getFloat("qz_"+number, 0.0f);
+        float qw = preferences.getFloat("qw_"+number, 1.0f);
+
+        mQuatScramble.set(qx,qy,qz,qw);
+        modifyCurrentPosition( mCurrentPosition, mQuatScramble);
+        }
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      long finishRotationNow(EffectListener listener)
+        {
+        int pointNum = mRotationAngle.getNumPoints();
+
+        if( pointNum>=1 )
           {
-          z = -z;
-          y = -y;
-          x = -x;
+          float startingAngle = mRotationAngle.getPoint(pointNum-1).get0();
+          int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+          mRotationAngleStatic.set0(startingAngle);
+          mRotationAngleFinal.set0(nearestAngleInDegrees);
+          mRotationAngleMiddle.set0( nearestAngleInDegrees + (nearestAngleInDegrees-startingAngle)*0.2f );
+          return setUpCallback(listener);
           }
-        else if( z==0 )
+        else
           {
-          if( y<0 )
-            {
-            y = -y;
-            x = -x;
-            }
-          else if( y==0 )
-            {
-            if( x<0 )
-              {
-              x = -x;
-              }
-            }
+          return 0;
           }
         }
 
-      mQuatScramble[i][j][k].set(x,y,z,w);
-      }
+    /////////////////////////////////////////////////////////////////////////////////
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      Static4D returnRotationQuat(float qx,float qy,float qz)
+        {
+        int pointNum = mRotationAngle.getNumPoints();
 
-    private boolean belongsToRotation(int x, int y, int z, int vector, int row)
-      {
-      switch(vector)
+        if( pointNum>=1 )
+          {
+          float startingAngle = mRotationAngle.getPoint(pointNum-1).get0();
+          int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+          double nearestAngleInRadians = nearestAngleInDegrees*Math.PI/180;
+          float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
+          float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
+          return new Static4D(qx*sinA, qy*sinA, qz*sinA, cosA);
+          }
+        else
+          {
+          return null;
+          }
+        }
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void removeRotationNow(Static4D quat)
         {
-        case VECTX: return mCurrentPosition[x][y][z].get0()==row;
-        case VECTY: return mCurrentPosition[x][y][z].get1()==row;
-        case VECTZ: return mCurrentPosition[x][y][z].get2()==row;
+        mRotationAngle.removeAll();
+        mQuatScramble.set(RubikSurfaceView.quatMultiply(quat,mQuatScramble));
+        normalizeScrambleQuat( mQuatScramble );
+        modifyCurrentPosition( mCurrentPosition,quat);
         }
 
-      return false;
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void releaseResources()
+        {
+        mCube.markForDeletion();
+        mNode.markForDeletion();
+        }
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void solve()
+        {
+        mQuatScramble.set(0,0,0,1);
+        mCurrentPosition.set(mOrigPosition);
+        }
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void beginNewRotation(Static3D axis)
+        {
+        mRotationAxis.set(axis);
+        mRotationAngle.add(mRotationAngleStatic);
+        }
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      void addNewRotation(Static3D axis, long durationMillis, int angle)
+        {
+        mRotationAxis.set(axis);
+        mRotationAngle.setDuration(durationMillis);
+        mRotationAngle.resetToBeginning();
+        mRotationAngle.add(new Static1D(0));
+        mRotationAngle.add(new Static1D(angle));
+        }
+
+
+    /////////////////////////////////////////////////////////////////////////////////
+
+      long setUpCallback(EffectListener listener)
+        {
+        mRotateEffect.notifyWhenFinished(listener);
+        return mRotateEffect.getID();
+        }
       }
 
+    private Cubit[][][] mCubits;
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.
 
-    private void modifyCurrentPosition(int x, int y, int z, Static4D quat)
+    float[] getLegalQuats()
       {
-      Static3D current = mCurrentPosition[x][y][z];
-      float diff = 0.5f*(mSize-1);
-      float cubitCenterX = current.get0() - diff;
-      float cubitCenterY = current.get1() - diff;
-      float cubitCenterZ = current.get2() - diff;
-
-      Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
-      Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
-
-      float rotatedX = rotatedCenter.get0() + diff;
-      float rotatedY = rotatedCenter.get1() + diff;
-      float rotatedZ = rotatedCenter.get2() + diff;
-
-      int roundedX = (int)(rotatedX+0.1f);
-      int roundedY = (int)(rotatedY+0.1f);
-      int roundedZ = (int)(rotatedZ+0.1f);
-
-      mCurrentPosition[x][y][z].set0(roundedX);
-      mCurrentPosition[x][y][z].set1(roundedY);
-      mCurrentPosition[x][y][z].set2(roundedZ);
+      final float SQ2 = 0.5f*((float)Math.sqrt(2));
+      return new float[] { 0.0f , 0.5f , -0.5f , 1.0f , -1.0f , SQ2 , -SQ2 };
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean belongsToRotation( Static3D currentPosition, int vector, int row)
+      {
+      switch(vector)
+        {
+        case VECTX: return currentPosition.get0()==row;
+        case VECTY: return currentPosition.get1()==row;
+        case VECTZ: return currentPosition.get2()==row;
+        }
+
+      return false;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -179,18 +254,7 @@ class RubikCube extends RubikObject
 
       mRotAxis = VECTX;
       mTexture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
-
-      mNodes          = new DistortedNode[mSize][mSize][mSize];
-      mCubes          = new MeshCubes[mSize][mSize][mSize];
-      mEffects        = new DistortedEffects[mSize][mSize][mSize];
-      mQuatScramble   = new Static4D[mSize][mSize][mSize];
-      mRotationAxis   = new Static3D[mSize][mSize][mSize];
-      mRotationAngle  = new Dynamic1D[mSize][mSize][mSize];
-      mCurrentPosition= new Static3D[mSize][mSize][mSize];
-      mRotateEffect   = new MatrixEffectRotate[mSize][mSize][mSize];
-
-      Static3D[][][] cubeVectors = new Static3D[mSize][mSize][mSize];
-      Static3D matrCenter = new Static3D(0,0,0);
+      mCubits  = new Cubit[mSize][mSize][mSize];
 
       // 3x2 bitmap = 6 squares:
       //
@@ -229,27 +293,11 @@ class RubikCube extends RubikObject
               tmpTop   = (y== mSize-1 ? mapTop   :mapBlack);
               tmpBottom= (y==       0 ? mapBottom:mapBlack);
 
-              mCubes[x][y][z]           = new MeshCubes(vertices,vertices,vertices, tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom);
-              cubeVectors[x][y][z]      = new Static3D( TEXTURE_SIZE*(x-nc), TEXTURE_SIZE*(y-nc), TEXTURE_SIZE*(z-nc) );
-              mQuatScramble[x][y][z]    = new Static4D(0,0,0,1);
-              mRotationAngle[x][y][z]   = new Dynamic1D();
-              mRotationAxis[x][y][z]    = new Static3D(1,0,0);
-              mCurrentPosition[x][y][z] = new Static3D(x,y,z);
-              mRotateEffect[x][y][z]    = new MatrixEffectRotate(mRotationAngle[x][y][z], mRotationAxis[x][y][z], matrCenter);
-
-              mEffects[x][y][z] = new DistortedEffects();
-              mEffects[x][y][z].apply(mSinkEffect);
-              mEffects[x][y][z].apply( new MatrixEffectMove(cubeVectors[x][y][z]) );
-              mEffects[x][y][z].apply( new MatrixEffectQuaternion(mQuatScramble[x][y][z], matrCenter));
-              mEffects[x][y][z].apply(mRotateEffect[x][y][z]);
-              mEffects[x][y][z].apply(mQuatAEffect);
-              mEffects[x][y][z].apply(mQuatCEffect);
-              mEffects[x][y][z].apply(mScaleEffect);
-              mEffects[x][y][z].apply(mMoveEffect);
-
-              mNodes[x][y][z] = new DistortedNode(mTexture,mEffects[x][y][z],mCubes[x][y][z]);
-
-              attach(mNodes[x][y][z]);
+              mCubits[x][y][z] = new Cubit(new MeshCubes(vertices,vertices,vertices, tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom),
+                                           new Static3D( TEXTURE_SIZE*(x-nc), TEXTURE_SIZE*(y-nc), TEXTURE_SIZE*(z-nc) ),
+                                           new Static3D(x,y,z) );
+
+              attach(mCubits[x][y][z].mNode);
               }
             }
       }
@@ -261,22 +309,12 @@ class RubikCube extends RubikObject
 
    public void savePreferences(SharedPreferences.Editor editor)
      {
-     float qx,qy,qz,qw;
-
      for(int x=0; x<mSize; x++)
         for(int y=0; y<mSize; y++)
           for(int z=0; z<mSize; z++)
             if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
               {
-              qx = mQuatScramble[x][y][z].get0();
-              qy = mQuatScramble[x][y][z].get1();
-              qz = mQuatScramble[x][y][z].get2();
-              qw = mQuatScramble[x][y][z].get3();
-
-              editor.putFloat("qx_"+x+"_"+y+"_"+z, qx);
-              editor.putFloat("qy_"+x+"_"+y+"_"+z, qy);
-              editor.putFloat("qz_"+x+"_"+y+"_"+z, qz);
-              editor.putFloat("qw_"+x+"_"+y+"_"+z, qw);
+              mCubits[x][y][z].savePreferences(editor);
               }
      }
 
@@ -284,20 +322,12 @@ class RubikCube extends RubikObject
 
    public void restorePreferences(SharedPreferences preferences)
      {
-     float qx,qy,qz,qw;
-
      for(int x=0; x<mSize; x++)
         for(int y=0; y<mSize; y++)
           for(int z=0; z<mSize; z++)
             if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
               {
-              qx = preferences.getFloat("qx_"+x+"_"+y+"_"+z, 0.0f);
-              qy = preferences.getFloat("qy_"+x+"_"+y+"_"+z, 0.0f);
-              qz = preferences.getFloat("qz_"+x+"_"+y+"_"+z, 0.0f);
-              qw = preferences.getFloat("qw_"+x+"_"+y+"_"+z, 1.0f);
-
-              mQuatScramble[x][y][z].set(qx,qy,qz,qw);
-              modifyCurrentPosition(x, y, z, mQuatScramble[x][y][z]);
+              mCubits[x][y][z].restorePreferences(preferences);
               }
      }
 
@@ -313,36 +343,15 @@ class RubikCube extends RubikObject
          for(int z=0; z<mSize; z++)
            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
              {
-             if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
+             if( belongsToRotation(mCubits[x][y][z].mCurrentPosition,mRotAxis,mRotRow) )
                {
                if( first )
                  {
                  first = false;
-                 mRotateEffect[x][y][z].notifyWhenFinished(listener);
-                 effectID = mRotateEffect[x][y][z].getID();
-                 int pointNum = mRotationAngle[x][y][z].getNumPoints();
-
-                 if( pointNum>=1 )
-                   {
-                   float startingAngle = mRotationAngle[x][y][z].getPoint(pointNum-1).get0();
-                   int nearestAngleInDegrees = computeNearestAngle(startingAngle);
-                   mRotationAngleStatic.set0(startingAngle);
-                   mRotationAngleFinal.set0(nearestAngleInDegrees);
-                   mRotationAngleMiddle.set0( nearestAngleInDegrees + (nearestAngleInDegrees-startingAngle)*0.2f );
-                   }
-                 else
-                   {
-                   android.util.Log.e("cube", "ERROR finishing rotation!");
-                   return 0;
-                   }
+                 effectID = mCubits[x][y][z].finishRotationNow(listener);
                  }
 
-               mRotationAngle[x][y][z].setDuration(POST_ROTATION_MILLISEC);
-               mRotationAngle[x][y][z].resetToBeginning();
-               mRotationAngle[x][y][z].removeAll();
-               mRotationAngle[x][y][z].add(mRotationAngleStatic);
-               mRotationAngle[x][y][z].add(mRotationAngleMiddle);
-               mRotationAngle[x][y][z].add(mRotationAngleFinal);
+               resetRotationAngle(mCubits[x][y][z].mRotationAngle);
                }
              }
 
@@ -364,8 +373,7 @@ class RubikCube extends RubikObject
            {
            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
              {
-             mCubes[x][y][z].markForDeletion();
-             mNodes[x][y][z].markForDeletion();
+             mCubits[x][y][z].releaseResources();
              }
            }
      }
@@ -424,7 +432,7 @@ class RubikCube extends RubikObject
            {
            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
              {
-             mEffects[x][y][z].apply(effect, position);
+             mCubits[x][y][z].mEffect.apply(effect, position);
              }
            }
       }
@@ -439,7 +447,7 @@ class RubikCube extends RubikObject
            {
            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
              {
-             mEffects[x][y][z].abortById(effectID);
+             mCubits[x][y][z].mEffect.abortById(effectID);
              }
            }
       }
@@ -453,8 +461,7 @@ class RubikCube extends RubikObject
          for(int z=0; z<mSize; z++)
            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
              {
-             mQuatScramble[x][y][z].set(0,0,0,1);
-             mCurrentPosition[x][y][z].set(x,y,z);
+             mCubits[x][y][z].solve();
              }
       }
 
@@ -462,7 +469,7 @@ class RubikCube extends RubikObject
 
    public boolean isSolved()
      {
-     Static4D q = mQuatScramble[0][0][0];
+     Static4D q = mCubits[0][0][0].mQuatScramble;
 
      float x = q.get0();
      float y = q.get1();
@@ -475,7 +482,7 @@ class RubikCube extends RubikObject
            {
            if( i==0 || i==mSize-1 || j==0 || j==mSize-1 || k==0 || k==mSize-1 )
              {
-             q = mQuatScramble[i][j][k];
+             q = mCubits[i][j][k].mQuatScramble;
 
              if( q.get0()!=x || q.get1()!=y || q.get2()!=z || q.get3()!=w )
                {
@@ -510,10 +517,9 @@ class RubikCube extends RubikObject
          for(int z=0; z<mSize; z++)
            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
              {
-             if( belongsToRotation(x,y,z,vector,mRotRow) )
+             if( belongsToRotation( mCubits[x][y][z].mCurrentPosition,vector,mRotRow) )
                {
-               mRotationAxis[x][y][z].set(axis);
-               mRotationAngle[x][y][z].add(mRotationAngleStatic);
+               mCubits[x][y][z].beginNewRotation(axis);
                }
              }
      }
@@ -543,19 +549,14 @@ class RubikCube extends RubikObject
           for(int z=0; z<mSize; z++)
             if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
               {
-              if( belongsToRotation(x,y,z,vector,mRotRow) )
+              if( belongsToRotation(mCubits[x][y][z].mCurrentPosition,vector,mRotRow) )
                 {
-                mRotationAxis[x][y][z].set(axis);
-                mRotationAngle[x][y][z].setDuration(durationMillis);
-                mRotationAngle[x][y][z].resetToBeginning();
-                mRotationAngle[x][y][z].add(new Static1D(0));
-                mRotationAngle[x][y][z].add(new Static1D(angle));
+                mCubits[x][y][z].addNewRotation(axis,durationMillis,angle);
 
                 if( first )
                   {
                   first = false;
-                  effectID = mRotateEffect[x][y][z].getID();
-                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
+                  effectID = mCubits[x][y][z].setUpCallback(listener);
                   }
                 }
               }
@@ -583,33 +584,15 @@ class RubikCube extends RubikObject
           for(int z=0; z<mSize; z++)
             if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
               {
-              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
+              if( belongsToRotation(mCubits[x][y][z].mCurrentPosition,mRotAxis,mRotRow) )
                 {
                 if( first )
                   {
                   first = false;
-                  int pointNum = mRotationAngle[x][y][z].getNumPoints();
-
-                  if( pointNum>=1 )
-                    {
-                    float startingAngle = mRotationAngle[x][y][z].getPoint(pointNum-1).get0();
-                    int nearestAngleInDegrees = computeNearestAngle(startingAngle);
-                    double nearestAngleInRadians = nearestAngleInDegrees*Math.PI/180;
-                    float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
-                    float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
-                    quat = new Static4D(qx*sinA, qy*sinA, qz*sinA, cosA);
-                    }
-                  else
-                    {
-                    android.util.Log.e("cube", "ERROR removing rotation!");
-                    return;
-                    }
+                  quat = mCubits[x][y][z].returnRotationQuat(qx,qy,qz);
                   }
 
-                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);
+                mCubits[x][y][z].removeRotationNow(quat);
                 }
               }
 
diff --git a/src/main/java/org/distorted/object/RubikObject.java b/src/main/java/org/distorted/object/RubikObject.java
index fa39bd8a..dec25916 100644
--- a/src/main/java/org/distorted/object/RubikObject.java
+++ b/src/main/java/org/distorted/object/RubikObject.java
@@ -31,18 +31,22 @@ import org.distorted.library.main.DistortedNode;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshRectangles;
 import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic1D;
 import org.distorted.library.type.Static1D;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
+import org.distorted.magic.RubikSurfaceView;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public abstract class RubikObject extends DistortedNode
   {
   static final float OBJECT_SCREEN_RATIO = 0.5f;
-  static final int POST_ROTATION_MILLISEC = 500;
   static final int TEXTURE_SIZE = 100;
 
+  private static final int POST_ROTATION_MILLISEC = 500;
+  private final float[] LEGAL_QUATS;
+
   private Static3D mMove, mScale, mNodeMove, mNodeScale;
   private Static4D mQuatAccumulated;
   private DistortedTexture mNodeTexture;
@@ -64,8 +68,8 @@ public abstract class RubikObject extends DistortedNode
     {
     super(texture,effects,mesh);
 
+    LEGAL_QUATS = getLegalQuats();
     mNodeTexture = texture;
-
     mSize = size;
 
     mRotationAngleStatic = new Static1D(0);
@@ -96,6 +100,69 @@ public abstract class RubikObject extends DistortedNode
     effects.apply(nodeMoveEffect);
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Because of quatMultiplication, errors can accumulate - so to avoid this, we
+// correct the value of the 'scramble' quat to what it should be - one of the legal quats from the
+// list LEGAL_QUATS.
+//
+// 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.
+
+    void normalizeScrambleQuat(Static4D quat)
+      {
+      float x = quat.get0();
+      float y = quat.get1();
+      float z = quat.get2();
+      float w = quat.get3();
+      float diff;
+
+      for(float legal: LEGAL_QUATS)
+        {
+        diff = x-legal;
+        if( diff*diff<0.01f ) x = legal;
+        diff = y-legal;
+        if( diff*diff<0.01f ) y = legal;
+        diff = z-legal;
+        if( diff*diff<0.01f ) z = legal;
+        diff = w-legal;
+        if( diff*diff<0.01f ) w = 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;
+              }
+            }
+          }
+        }
+
+      quat.set(x,y,z,w);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   int computeNearestAngle(float angle)
@@ -108,6 +175,41 @@ public abstract class RubikObject extends DistortedNode
     return NEAREST*tmp;
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void resetRotationAngle(Dynamic1D rotationAngle)
+    {
+    rotationAngle.setDuration(POST_ROTATION_MILLISEC);
+    rotationAngle.resetToBeginning();
+    rotationAngle.removeAll();
+    rotationAngle.add(mRotationAngleStatic);
+    rotationAngle.add(mRotationAngleMiddle);
+    rotationAngle.add(mRotationAngleFinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void modifyCurrentPosition(Static3D currentPosition, Static4D quat)
+    {
+    float diff = 0.5f*(mSize-1);
+    float cubitCenterX = currentPosition.get0() - diff;
+    float cubitCenterY = currentPosition.get1() - diff;
+    float cubitCenterZ = currentPosition.get2() - diff;
+
+    Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
+    Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
+
+    float rotatedX = rotatedCenter.get0() + diff;
+    float rotatedY = rotatedCenter.get1() + diff;
+    float rotatedZ = rotatedCenter.get2() + diff;
+
+    int roundedX = (int)(rotatedX+0.1f);
+    int roundedY = (int)(rotatedY+0.1f);
+    int roundedZ = (int)(rotatedZ+0.1f);
+
+    currentPosition.set(roundedX, roundedY, roundedZ);
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private float getSinkStrength()
@@ -173,6 +275,8 @@ public abstract class RubikObject extends DistortedNode
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  abstract float[] getLegalQuats();
+
   public abstract void savePreferences(SharedPreferences.Editor editor);
   public abstract void restorePreferences(SharedPreferences preferences);
 
