commit 47ba5ddcd54595f61feba7e484dbb1ff4fa1e776
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Jan 24 16:29:45 2020 +0000

    RubikCube: further fixes

diff --git a/src/main/java/org/distorted/component/HorizontalNumberPicker.java b/src/main/java/org/distorted/component/HorizontalNumberPicker.java
index 2a5f0ca5..40f59130 100644
--- a/src/main/java/org/distorted/component/HorizontalNumberPicker.java
+++ b/src/main/java/org/distorted/component/HorizontalNumberPicker.java
@@ -36,26 +36,6 @@ public class HorizontalNumberPicker extends LinearLayout
   private TextView mNumber;
   private int mMin, mMax;
 
-  public HorizontalNumberPicker(Context context, @Nullable AttributeSet attrs)
-    {
-    super(context, attrs);
-
-    mMin = 0;
-    mMax = 5;
-
-    inflate(context, R.layout.numberpicker, this);
-
-    mNumber = findViewById(R.id.textNumber);
-
-    final Button btn_less = findViewById(R.id.buttonLess);
-    btn_less.setOnClickListener(new AddHandler(-1));
-
-    final Button btn_more = findViewById(R.id.buttonMore);
-    btn_more.setOnClickListener(new AddHandler( 1));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
   private class AddHandler implements OnClickListener
     {
     final int diff;
@@ -82,6 +62,28 @@ public class HorizontalNumberPicker extends LinearLayout
       }
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public HorizontalNumberPicker(Context context, @Nullable AttributeSet attrs)
+    {
+    super(context, attrs);
+
+    mMin = 0;
+    mMax = 5;
+
+    inflate(context, R.layout.numberpicker, this);
+
+    mNumber = findViewById(R.id.textNumber);
+
+    final Button btn_less = findViewById(R.id.buttonLess);
+    btn_less.setOnClickListener(new AddHandler(-1));
+
+    final Button btn_more = findViewById(R.id.buttonMore);
+    btn_more.setOnClickListener(new AddHandler( 1));
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public int getValue()
@@ -111,13 +113,6 @@ public class HorizontalNumberPicker extends LinearLayout
       }
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getMin()
-    {
-    return mMin;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void setMin(int min)
@@ -125,13 +120,6 @@ public class HorizontalNumberPicker extends LinearLayout
     mMin = min;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getMax()
-    {
-    return mMax;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void setMax(int max)
diff --git a/src/main/java/org/distorted/effect/BaseEffect.java b/src/main/java/org/distorted/effect/BaseEffect.java
index 91f6f15a..01e43c4d 100644
--- a/src/main/java/org/distorted/effect/BaseEffect.java
+++ b/src/main/java/org/distorted/effect/BaseEffect.java
@@ -37,7 +37,7 @@ public class BaseEffect
     {
     SIZECHANGE  ( 20, 1, R.string.sizechange_effect , SizeChangeEffect.class),
     SOLVE       ( 20, 1, R.string.solve_effect      , SolveEffect.class     ),
-    SCRAMBLE    ( 20, 1, R.string.scramble_effect   , ScrambleEffect.class  ),
+    SCRAMBLE    ( 30, 1, R.string.scramble_effect   , ScrambleEffect.class  ),
     WIN         ( 20, 1, R.string.win_effect        , WinEffect.class       ),
     ;
 
diff --git a/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java b/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java
index a4f63f06..e87d6f0c 100644
--- a/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java
+++ b/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java
@@ -85,39 +85,19 @@ public abstract class ScrambleEffect extends BaseEffect implements EffectListene
     mLastVector = -1;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static String[] getNames()
-    {
-    String[] names = new String[NUM_EFFECTS];
-
-    for( int i=0; i<NUM_EFFECTS; i++)
-      {
-      names[i] = types[i].name();
-      }
-
-    return names;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScrambleEffect create(int ordinal) throws InstantiationException, IllegalAccessException
-    {
-    return types[ordinal].effect.newInstance();
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   abstract void createEffects(int duration);
   abstract void effectFinishedPlugin(final long effectID);
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// first compute how many out of 'numScrambles' are double turns (this will matter when we compute
+// the time a single quarter-turn takes!
 
   private void createBaseEffects(int duration, int numScrambles)
     {
     mNumScramblesLeft = numScrambles;
 
-    // compute how many out of 'numScrambles' are double turns.
     mNumDoubleScramblesLeft=0;
 
     for(int i=0; i<numScrambles; i++)
@@ -197,6 +177,67 @@ public abstract class ScrambleEffect extends BaseEffect implements EffectListene
     return sign==0 ? result : -result;
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void assignEffects()
+    {
+    for(int i=0; i<mCubeEffectNumber; i++)
+      {
+      mCube.apply(mCubeEffects[i],mCubeEffectPosition[i]);
+      mCubeEffects[i].notifyWhenFinished(this);
+      }
+
+    DistortedEffects nodeEffects = mCube.getEffects();
+
+    for(int i=0; i<mNodeEffectNumber; i++)
+      {
+      nodeEffects.apply(mNodeEffects[i],mNodeEffectPosition[i]);
+      mNodeEffects[i].notifyWhenFinished(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void disassignEffects()
+    {
+    for(int i=0; i<mCubeEffectNumber; i++)
+      {
+      mCube.remove(mCubeEffects[i].getID());
+      }
+
+    DistortedEffects nodeEffects = mCube.getEffects();
+
+    for(int i=0; i<mNodeEffectNumber; i++)
+      {
+      nodeEffects.abortById(mNodeEffects[i].getID());
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static String[] getNames()
+    {
+    String[] names = new String[NUM_EFFECTS];
+
+    for( int i=0; i<NUM_EFFECTS; i++)
+      {
+      names[i] = types[i].name();
+      }
+
+    return names;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static ScrambleEffect create(int ordinal) throws InstantiationException, IllegalAccessException
+    {
+    return types[ordinal].effect.newInstance();
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void effectFinished(final long effectID)
@@ -257,6 +298,7 @@ public abstract class ScrambleEffect extends BaseEffect implements EffectListene
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  @SuppressWarnings("unused")
   public long start(int duration, RubikRenderer renderer)
     {
     mCube     = renderer.getCube();
@@ -277,42 +319,6 @@ public abstract class ScrambleEffect extends BaseEffect implements EffectListene
     return FAKE_EFFECT_ID;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void assignEffects()
-    {
-    for(int i=0; i<mCubeEffectNumber; i++)
-      {
-      mCube.apply(mCubeEffects[i],mCubeEffectPosition[i]);
-      mCubeEffects[i].notifyWhenFinished(this);
-      }
-
-    DistortedEffects nodeEffects = mCube.getEffects();
-
-    for(int i=0; i<mNodeEffectNumber; i++)
-      {
-      nodeEffects.apply(mNodeEffects[i],mNodeEffectPosition[i]);
-      mNodeEffects[i].notifyWhenFinished(this);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void disassignEffects()
-    {
-    for(int i=0; i<mCubeEffectNumber; i++)
-      {
-      mCube.remove(mCubeEffects[i].getID());
-      }
-
-    DistortedEffects nodeEffects = mCube.getEffects();
-
-    for(int i=0; i<mNodeEffectNumber; i++)
-      {
-      nodeEffects.abortById(mNodeEffects[i].getID());
-      }
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   @SuppressWarnings("unused")
diff --git a/src/main/java/org/distorted/effect/scramble/ScrambleEffectRotations.java b/src/main/java/org/distorted/effect/scramble/ScrambleEffectRotations.java
index 7d7d9303..2f0b3211 100644
--- a/src/main/java/org/distorted/effect/scramble/ScrambleEffectRotations.java
+++ b/src/main/java/org/distorted/effect/scramble/ScrambleEffectRotations.java
@@ -38,6 +38,20 @@ public class ScrambleEffectRotations extends ScrambleEffect
   {
   private Random mRnd = new Random(0);
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Static4D generateNewRandomPoint()
+    {
+    float x = mRnd.nextFloat();
+    float y = mRnd.nextFloat();
+    float z = mRnd.nextFloat();
+    float w = mRnd.nextFloat();
+
+    float len = (float)Math.sqrt(x*x + y*y + z*z + w*w);
+
+    return new Static4D( x/len, y/len, z/len, w/len);
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void createEffects(int duration)
@@ -69,20 +83,6 @@ public class ScrambleEffectRotations extends ScrambleEffect
     mCubeEffects[1] = new MatrixEffectMove(d0);
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Static4D generateNewRandomPoint()
-    {
-    float x = mRnd.nextFloat();
-    float y = mRnd.nextFloat();
-    float z = mRnd.nextFloat();
-    float w = mRnd.nextFloat();
-
-    float len = (float)Math.sqrt(x*x + y*y + z*z + w*w);
-
-    return new Static4D( x/len, y/len, z/len, w/len);
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void effectFinishedPlugin(final long effectID)
diff --git a/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java b/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java
index 0d6c1621..d4c095a2 100644
--- a/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java
+++ b/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java
@@ -96,40 +96,11 @@ public abstract class SizeChangeEffect extends BaseEffect implements EffectListe
     mCube               = new RubikCube[NUM_PHASES];
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static String[] getNames()
-    {
-    String[] names = new String[NUM_EFFECTS];
-
-    for( int i=0; i<NUM_EFFECTS; i++)
-      {
-      names[i] = types[i].name();
-      }
-
-    return names;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static SizeChangeEffect create(int ordinal) throws InstantiationException, IllegalAccessException
-    {
-    return types[ordinal].effect.newInstance();
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   abstract int createEffectsPhase0(int duration);
   abstract int createEffectsPhase1(int duration);
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void effectFinished(final long effectID)
-    {
-    if( mPhaseActive[0] ) effectFinishedPhase(effectID,0);
-    if( mPhaseActive[1] ) effectFinishedPhase(effectID,1);
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void effectFinishedPhase(final long effectID, int phase)
@@ -190,33 +161,6 @@ public abstract class SizeChangeEffect extends BaseEffect implements EffectListe
       }
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public long start(int duration, RubikRenderer renderer)
-    {
-    mScreen   = renderer.getScreen();
-    mCube[0]  = renderer.getOldCube();
-    mCube[1]  = renderer.getCube();
-    mListener = renderer;
-    mDuration = duration;
-
-    if( mCube[0]!=null )
-      {
-      mPhaseActive[0] = true;
-      mEffectFinished[0] = createEffectsPhase0(mDuration);
-      assignEffects(0);
-      }
-    else
-      {
-      mPhaseActive[1] = true;
-      mEffectFinished[1] = createEffectsPhase1(mDuration);
-      assignEffects(1);
-      mScreen.attach(mCube[1]);
-      }
-
-    return FAKE_EFFECT_ID;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void assignEffects(int phase)
@@ -244,8 +188,70 @@ public abstract class SizeChangeEffect extends BaseEffect implements EffectListe
       }
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void effectFinished(final long effectID)
+    {
+    if( mPhaseActive[0] ) effectFinishedPhase(effectID,0);
+    if( mPhaseActive[1] ) effectFinishedPhase(effectID,1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static String[] getNames()
+    {
+    String[] names = new String[NUM_EFFECTS];
+
+    for( int i=0; i<NUM_EFFECTS; i++)
+      {
+      names[i] = types[i].name();
+      }
+
+    return names;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static SizeChangeEffect create(int ordinal) throws InstantiationException, IllegalAccessException
+    {
+    return types[ordinal].effect.newInstance();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public long start(int duration, RubikRenderer renderer)
+    {
+    mScreen   = renderer.getScreen();
+    mCube[0]  = renderer.getOldCube();
+    mCube[1]  = renderer.getCube();
+    mListener = renderer;
+    mDuration = duration;
+
+    if( mCube[0]!=null )
+      {
+      mPhaseActive[0] = true;
+      mEffectFinished[0] = createEffectsPhase0(mDuration);
+      assignEffects(0);
+      }
+    else
+      {
+      mPhaseActive[1] = true;
+      mEffectFinished[1] = createEffectsPhase1(mDuration);
+      assignEffects(1);
+      mScreen.attach(mCube[1]);
+      }
+
+    return FAKE_EFFECT_ID;
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  @SuppressWarnings("unused")
   public static void enableEffects()
     {
     Method method;
diff --git a/src/main/java/org/distorted/effect/solve/SolveEffect.java b/src/main/java/org/distorted/effect/solve/SolveEffect.java
index 02da2fa5..b517194d 100644
--- a/src/main/java/org/distorted/effect/solve/SolveEffect.java
+++ b/src/main/java/org/distorted/effect/solve/SolveEffect.java
@@ -80,7 +80,7 @@ public abstract class SolveEffect extends BaseEffect implements EffectListener
 
   SolveEffect()
     {
-    mPhase        =  0;
+    mPhase= 0;
 
     mCubeEffectNumber   = new int[NUM_PHASES];
     mNodeEffectNumber   = new int[NUM_PHASES];
@@ -92,6 +92,58 @@ public abstract class SolveEffect extends BaseEffect implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  abstract void createEffectsPhase0(int duration);
+  abstract void createEffectsPhase1(int duration);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void assignEffects(int phase)
+    {
+    mCubeEffectNumber[phase] = ( mCubeEffects[phase]!=null ) ? mCubeEffects[phase].length : 0;
+    mNodeEffectNumber[phase] = ( mNodeEffects[phase]!=null ) ? mNodeEffects[phase].length : 0;
+
+    if( mCubeEffectNumber[phase]==0 && mNodeEffectNumber[phase]==0 )
+      {
+      throw new RuntimeException("Cube and Node Effects ("+phase+" phase) both not created!");
+      }
+
+    for(int i=0; i<mCubeEffectNumber[phase]; i++)
+      {
+      mCube.apply(mCubeEffects[phase][i],mCubeEffectPosition[phase][i]);
+      mCubeEffects[phase][i].notifyWhenFinished(this);
+      }
+
+    DistortedEffects nodeEffects = mCube.getEffects();
+
+    for(int i=0; i<mNodeEffectNumber[phase]; i++)
+      {
+      nodeEffects.apply(mNodeEffects[phase][i],mNodeEffectPosition[phase][i]);
+      mNodeEffects[phase][i].notifyWhenFinished(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void effectAction(int phase)
+    {
+    switch(phase)
+      {
+      case 0: mEffectReturned = 0;
+              mPhase          = 1;
+              mCube.solve();
+              createEffectsPhase1(mDuration);
+              assignEffects(mPhase);
+              break;
+      case 1: mListener.effectFinished(FAKE_EFFECT_ID);
+              break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
   public static String[] getNames()
     {
     String[] names = new String[NUM_EFFECTS];
@@ -106,16 +158,12 @@ public abstract class SolveEffect extends BaseEffect implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  @SuppressWarnings("unused")
   public static SolveEffect create(int ordinal) throws InstantiationException, IllegalAccessException
     {
     return types[ordinal].effect.newInstance();
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract void createEffectsPhase0(int duration);
-  abstract void createEffectsPhase1(int duration);
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void effectFinished(final long effectID)
@@ -148,23 +196,7 @@ public abstract class SolveEffect extends BaseEffect implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void effectAction(int phase)
-    {
-    switch(phase)
-      {
-      case 0: mEffectReturned = 0;
-              mPhase          = 1;
-              mCube.solve();
-              createEffectsPhase1(mDuration);
-              assignEffects(mPhase);
-              break;
-      case 1: mListener.effectFinished(FAKE_EFFECT_ID);
-              break;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
+  @SuppressWarnings("unused")
   public long start(int duration, RubikRenderer renderer)
     {
     mScreen   = renderer.getScreen();
@@ -178,33 +210,6 @@ public abstract class SolveEffect extends BaseEffect implements EffectListener
     return FAKE_EFFECT_ID;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void assignEffects(int phase)
-    {
-    mCubeEffectNumber[phase] = ( mCubeEffects[phase]!=null ) ? mCubeEffects[phase].length : 0;
-    mNodeEffectNumber[phase] = ( mNodeEffects[phase]!=null ) ? mNodeEffects[phase].length : 0;
-
-    if( mCubeEffectNumber[phase]==0 && mNodeEffectNumber[phase]==0 )
-      {
-      throw new RuntimeException("Cube and Node Effects ("+phase+" phase) both not created!");
-      }
-
-    for(int i=0; i<mCubeEffectNumber[phase]; i++)
-      {
-      mCube.apply(mCubeEffects[phase][i],mCubeEffectPosition[phase][i]);
-      mCubeEffects[phase][i].notifyWhenFinished(this);
-      }
-
-    DistortedEffects nodeEffects = mCube.getEffects();
-
-    for(int i=0; i<mNodeEffectNumber[phase]; i++)
-      {
-      nodeEffects.apply(mNodeEffects[phase][i],mNodeEffectPosition[phase][i]);
-      mNodeEffects[phase][i].notifyWhenFinished(this);
-      }
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   @SuppressWarnings("unused")
diff --git a/src/main/java/org/distorted/effect/solve/SolveEffectSpin.java b/src/main/java/org/distorted/effect/solve/SolveEffectSpin.java
index 574c35d2..ce438519 100644
--- a/src/main/java/org/distorted/effect/solve/SolveEffectSpin.java
+++ b/src/main/java/org/distorted/effect/solve/SolveEffectSpin.java
@@ -25,49 +25,89 @@ import org.distorted.library.type.Dynamic;
 import org.distorted.library.type.Dynamic1D;
 import org.distorted.library.type.Static1D;
 import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public class SolveEffectSpin extends SolveEffect
   {
-  public void createEffectsPhase0(int duration)
+  private static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
     {
-    mCubeEffectPosition[0] = new int[] {3};
-    mCubeEffects[0]        = new Effect[mCubeEffectPosition[0].length];
+    float qx = quat1.get1();
+    float qy = quat1.get2();
+    float qz = quat1.get3();
+    float qw = quat1.get4();
 
-    Static3D axis  = new Static3D(1,0,0);
-    Static3D center= new Static3D(0,0,0);
+    float rx = quat2.get1();
+    float ry = quat2.get2();
+    float rz = quat2.get3();
+    float rw = quat2.get4();
 
-    Dynamic1D d0 = new Dynamic1D(duration/2, 1.0f);
-    d0.setMode(Dynamic.MODE_JUMP);
-    d0.setConvexity(0.0f);          // otherwise speed of the rotation would be strangely uneven
-    d0.add(new Static1D( 0*36));
-    d0.add(new Static1D( 1*36));
-    d0.add(new Static1D( 3*36));
-    d0.add(new Static1D( 6*36));
-    d0.add(new Static1D(10*36));
-    mCubeEffects[0][0] = new MatrixEffectRotate(d0,axis,center);
+    float tx = rw*qx - rz*qy + ry*qz + rx*qw;
+    float ty = rw*qy + rz*qx + ry*qw - rx*qz;
+    float tz = rw*qz + rz*qw - ry*qx + rx*qy;
+    float tw = rw*qw - rz*qz - ry*qy - rx*qx;
+
+    return new Static4D(tx,ty,tz,tw);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
 
-  public void createEffectsPhase1(int duration)
+  private static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
+    {
+    float qx = quat.get1();
+    float qy = quat.get2();
+    float qz = quat.get3();
+    float qw = quat.get4();
+
+    Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+    Static4D tmp = quatMultiply(quatInverted,vector);
+
+    return quatMultiply(tmp,quat);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createEffects(int phase, int duration, int[] points)
     {
-    mCubeEffectPosition[1] = new int[] {3};
-    mCubeEffects[1]        = new Effect[mCubeEffectPosition[1].length];
+    mCubeEffectPosition[phase] = new int[] {3};
+    mCubeEffects[phase]        = new Effect[mCubeEffectPosition[0].length];
 
-    Static3D axis  = new Static3D(1,0,0);
+    Static4D quaternion = mCube.getRotationQuat();                        // always rotate around
+    Static4D tmpAxis    = new Static4D(0,1,0,0);                          // vert axis no matter
+    Static4D rotated    = rotateVectorByInvertedQuat(tmpAxis,quaternion); // how cube is rotated
+
+    Static3D axis  = new Static3D(rotated.get1(), rotated.get2(), rotated.get3());
     Static3D center= new Static3D(0,0,0);
 
-    Dynamic1D d1 = new Dynamic1D(duration/2, 1.0f);
-    d1.setMode(Dynamic.MODE_JUMP);
-    d1.setConvexity(0.0f);
-    d1.add(new Static1D( 0*36));
-    d1.add(new Static1D( 4*36));
-    d1.add(new Static1D( 7*36));
-    d1.add(new Static1D( 9*36));
-    d1.add(new Static1D(10*36));
-    mCubeEffects[1][0] = new MatrixEffectRotate(d1,axis,center);
+    Dynamic1D d = new Dynamic1D(duration/2, 1.0f);
+    d.setMode(Dynamic.MODE_JUMP);
+    d.setConvexity(0.0f);   // otherwise speed of the rotation would be strangely uneven
+
+    d.add( new Static1D(36*points[0]) );
+    d.add( new Static1D(36*points[1]) );
+    d.add( new Static1D(36*points[2]) );
+    d.add( new Static1D(36*points[3]) );
+    d.add( new Static1D(36*points[4]) );
+
+    mCubeEffects[phase][0] = new MatrixEffectRotate(d,axis,center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void createEffectsPhase0(int duration)
+    {
+    createEffects(0,duration,new int[] {0,1,3,6,10});
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void createEffectsPhase1(int duration)
+    {
+    createEffects(1,duration,new int[] {0,4,7,9,10});
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/effect/win/WinEffect.java b/src/main/java/org/distorted/effect/win/WinEffect.java
index acd4cd65..b27a9d09 100644
--- a/src/main/java/org/distorted/effect/win/WinEffect.java
+++ b/src/main/java/org/distorted/effect/win/WinEffect.java
@@ -36,7 +36,6 @@ public abstract class WinEffect extends BaseEffect implements EffectListener
   public enum Type
     {
     NONE   (WinEffectNone.class),
-    SPIN   (WinEffectSpin.class),
     GLOW   (WinEffectGlow.class),
     ;
 
@@ -77,6 +76,40 @@ public abstract class WinEffect extends BaseEffect implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  abstract void createEffects(int duration);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void assignEffects()
+    {
+    mCubeEffectNumber = ( mCubeEffects!=null ) ? mCubeEffects.length : 0;
+    mNodeEffectNumber = ( mNodeEffects!=null ) ? mNodeEffects.length : 0;
+
+    if( mCubeEffectNumber==0 && mNodeEffectNumber==0 )
+      {
+      throw new RuntimeException("Cube and Node Effects both not created!");
+      }
+
+    for(int i=0; i<mCubeEffectNumber; i++)
+      {
+      mCube.apply(mCubeEffects[i],mCubeEffectPosition[i]);
+      mCubeEffects[i].notifyWhenFinished(this);
+      }
+
+    DistortedEffects nodeEffects = mCube.getEffects();
+
+    for(int i=0; i<mNodeEffectNumber; i++)
+      {
+      nodeEffects.apply(mNodeEffects[i],mNodeEffectPosition[i]);
+      mNodeEffects[i].notifyWhenFinished(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
   public static String[] getNames()
     {
     String[] names = new String[NUM_EFFECTS];
@@ -91,15 +124,12 @@ public abstract class WinEffect extends BaseEffect implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  @SuppressWarnings("unused")
   public static WinEffect create(int ordinal) throws InstantiationException, IllegalAccessException
     {
     return types[ordinal].effect.newInstance();
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract void createEffects(int duration);
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void effectFinished(final long effectID)
@@ -132,6 +162,7 @@ public abstract class WinEffect extends BaseEffect implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+  @SuppressWarnings("unused")
   public long start(int duration, RubikRenderer renderer)
     {
     mScreen   = renderer.getScreen();
@@ -145,33 +176,6 @@ public abstract class WinEffect extends BaseEffect implements EffectListener
     return FAKE_EFFECT_ID;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void assignEffects()
-    {
-    mCubeEffectNumber = ( mCubeEffects!=null ) ? mCubeEffects.length : 0;
-    mNodeEffectNumber = ( mNodeEffects!=null ) ? mNodeEffects.length : 0;
-
-    if( mCubeEffectNumber==0 && mNodeEffectNumber==0 )
-      {
-      throw new RuntimeException("Cube and Node Effects both not created!");
-      }
-
-    for(int i=0; i<mCubeEffectNumber; i++)
-      {
-      mCube.apply(mCubeEffects[i],mCubeEffectPosition[i]);
-      mCubeEffects[i].notifyWhenFinished(this);
-      }
-
-    DistortedEffects nodeEffects = mCube.getEffects();
-
-    for(int i=0; i<mNodeEffectNumber; i++)
-      {
-      nodeEffects.apply(mNodeEffects[i],mNodeEffectPosition[i]);
-      mNodeEffects[i].notifyWhenFinished(this);
-      }
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   @SuppressWarnings("unused")
diff --git a/src/main/java/org/distorted/effect/win/WinEffectSpin.java b/src/main/java/org/distorted/effect/win/WinEffectSpin.java
deleted file mode 100644
index 3dec9ee7..00000000
--- a/src/main/java/org/distorted/effect/win/WinEffectSpin.java
+++ /dev/null
@@ -1,65 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2019 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted is free software: you can redistribute it and/or modify                             //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted is distributed in the hope that it will be useful,                                  //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.effect.win;
-
-import org.distorted.library.effect.Effect;
-import org.distorted.library.effect.MatrixEffectRotate;
-import org.distorted.library.type.Dynamic;
-import org.distorted.library.type.Dynamic1D;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class WinEffectSpin extends WinEffect
-  {
-  public void createEffects(int duration)
-    {
-    mCubeEffectPosition = new int[] {3};
-    mCubeEffects        = new Effect[mCubeEffectPosition.length];
-
-    Static3D axis  = new Static3D(1,0,0);
-    Static3D center= new Static3D(0,0,0);
-
-    Dynamic1D d0 = new Dynamic1D(duration/2, 1.0f);
-    d0.setMode(Dynamic.MODE_JUMP);
-    d0.setConvexity(0.0f);          // otherwise speed of the rotation would be strangely uneven
-    d0.add(new Static1D( 0*36));
-    d0.add(new Static1D( 1*36));
-    d0.add(new Static1D( 3*36));
-    d0.add(new Static1D( 6*36));
-    d0.add(new Static1D(10*36));
-    d0.add(new Static1D(14*36));
-    d0.add(new Static1D(17*36));
-    d0.add(new Static1D(19*36));
-    d0.add(new Static1D(20*36));
-
-    mCubeEffects[0] = new MatrixEffectRotate(d0,axis,center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Enable all effects used in this Effect. Called by reflection from the parent class.
-
-  @SuppressWarnings("unused")
-  static void enable()
-    {
-
-    }
-  }
diff --git a/src/main/java/org/distorted/magic/RubikActivity.java b/src/main/java/org/distorted/magic/RubikActivity.java
index 405e6e80..f01020d9 100644
--- a/src/main/java/org/distorted/magic/RubikActivity.java
+++ b/src/main/java/org/distorted/magic/RubikActivity.java
@@ -42,11 +42,73 @@ public class RubikActivity extends AppCompatActivity
     private static final int[] button_ids  = {R.id.rubikSize2, R.id.rubikSize3, R.id.rubikSize4};
 
     public static final int MIN_SCRAMBLE =  1;
+    public static final int DEF_SCRAMBLE =  3;
     public static final int MAX_SCRAMBLE = 10;
 
     private static int mSize = DEFAULT_SIZE;
     private HorizontalNumberPicker mPicker;
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void markButton(int size)
+      {
+      mSize = size;
+
+      for(int b=0; b<button_ids.length; b++)
+        {
+        Drawable d = findViewById(button_ids[b]).getBackground();
+
+        if( size == b+SMALLEST_SIZE )
+          {
+          d.setColorFilter(ContextCompat.getColor(this,R.color.red), PorterDuff.Mode.MULTIPLY);
+          }
+        else
+          {
+          d.clearColorFilter();
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void savePreferences()
+      {
+      SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+      SharedPreferences.Editor editor = preferences.edit();
+
+      for (int i=0; i< BaseEffect.Type.LENGTH; i++)
+        {
+        BaseEffect.Type.getType(i).savePreferences(editor);
+        }
+
+      editor.putInt("scramble", mPicker.getValue() );
+
+      editor.apply();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void restorePreferences()
+      {
+      SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+      for (int i=0; i< BaseEffect.Type.LENGTH; i++)
+        {
+        BaseEffect.Type.getType(i).restorePreferences(preferences);
+        }
+
+      int scramble= preferences.getInt("scramble", DEF_SCRAMBLE);
+
+      mPicker.setValue(scramble);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    static int getSize()
+      {
+      return mSize;
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     @Override
@@ -96,12 +158,7 @@ public class RubikActivity extends AppCompatActivity
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    static int getSize()
-      {
-      return mSize;
-      }
-
+// PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void Settings(View v)
@@ -157,58 +214,4 @@ public class RubikActivity extends AppCompatActivity
         markButton(size);
         }
       }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void markButton(int size)
-     {
-     mSize = size;
-
-     for(int b=0; b<button_ids.length; b++)
-       {
-       Drawable d = findViewById(button_ids[b]).getBackground();
-
-       if( size == b+SMALLEST_SIZE )
-         {
-         d.setColorFilter(ContextCompat.getColor(this,R.color.red), PorterDuff.Mode.MULTIPLY);
-         }
-       else
-         {
-         d.clearColorFilter();
-         }
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void savePreferences()
-     {
-     SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
-     SharedPreferences.Editor editor = preferences.edit();
-
-     for (int i=0; i< BaseEffect.Type.LENGTH; i++)
-       {
-       BaseEffect.Type.getType(i).savePreferences(editor);
-       }
-
-     editor.putInt("scramble", mPicker.getValue() );
-
-     editor.apply();
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void restorePreferences()
-     {
-     SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
-
-     for (int i=0; i< BaseEffect.Type.LENGTH; i++)
-       {
-       BaseEffect.Type.getType(i).restorePreferences(preferences);
-       }
-
-     int scramble= preferences.getInt("scramble", MIN_SCRAMBLE);
-
-     mPicker.setValue(scramble);
-     }
 }
diff --git a/src/main/java/org/distorted/magic/RubikCube.java b/src/main/java/org/distorted/magic/RubikCube.java
index 0e5dfb8d..2d445167 100644
--- a/src/main/java/org/distorted/magic/RubikCube.java
+++ b/src/main/java/org/distorted/magic/RubikCube.java
@@ -24,13 +24,11 @@ import android.graphics.Canvas;
 import android.graphics.Paint;
 
 import org.distorted.library.effect.Effect;
-import org.distorted.library.effect.EffectType;
 import org.distorted.library.effect.MatrixEffectMove;
 import org.distorted.library.effect.MatrixEffectQuaternion;
 import org.distorted.library.effect.MatrixEffectRotate;
 import org.distorted.library.effect.MatrixEffectScale;
 import org.distorted.library.effect.VertexEffectSink;
-import org.distorted.library.effectqueue.EffectQueue;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedNode;
 import org.distorted.library.main.DistortedTexture;
@@ -67,6 +65,7 @@ public class RubikCube extends DistortedNode
     private MatrixEffectRotate[][][] mRotateEffect;
     private Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
     private Static3D mMove, mScale, mNodeMove, mNodeScale;
+    private Static4D mQuatAccumulated;
     private DistortedTexture mTexture;
 
     private int mRotAxis, mRotRow;
@@ -93,6 +92,8 @@ public class RubikCube extends DistortedNode
       mNodeMove = new Static3D(0,0,0);
       mNodeScale= new Static3D(1,1,1);
 
+      mQuatAccumulated = quatAcc;
+
       mRotAxis = VECTX;
       mTexture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
 
@@ -187,180 +188,167 @@ public class RubikCube extends DistortedNode
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    boolean isSolved()
+    private int computeNearestAngle(float angle)
       {
-      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];
+      final int NEAREST = 90;
 
-              if( q.get1()!=x || q.get2()!=y || q.get3()!=z || q.get4()!=w )
-                {
-                return false;
-                }
-              }
-            }
+      int tmp = (int)((angle+NEAREST/2)/NEAREST);
+      if( angle< -(NEAREST/2) ) tmp-=1;
 
-      return true;
+      return NEAREST*tmp;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.
 
-    public void apply(Effect effect, int position)
+    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)
       {
-      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 )
-              {
-              mEffects[x][y][z].apply(effect, position);
-              }
-            }
-      }
+      Static4D quat = mQuatScramble[i][j][k];
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      float x = quat.get1();
+      float y = quat.get2();
+      float z = quat.get3();
+      float w = quat.get4();
+      float diff;
 
-    public void remove(long effectID)
-      {
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
+      for(float legal: LEGAL)
+        {
+        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 )
             {
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+            y = -y;
+            x = -x;
+            }
+          else if( y==0 )
+            {
+            if( x<0 )
               {
-              mEffects[x][y][z].abortById(effectID);
+              x = -x;
               }
             }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
+          }
+        }
 
-    public void solve()
-      {
-      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 )
-              {
-              mQuatScramble[x][y][z].set(0,0,0,1);
-              mCurrentPosition[x][y][z].set(x,y,z);
-              }
+      mQuatScramble[i][j][k].set(x,y,z,w);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public String print_effects()
+    private float getSinkStrength()
       {
-      String str="";
-
-      EffectQueue[] effects = mEffects[0][0][0].getQueues();
-      EffectQueue matrix      = effects[0];
-      EffectQueue vertex      = effects[1];
-      EffectQueue fragment    = effects[2];
-      EffectQueue postprocess = effects[3];
-
-      str+="MATRIX: ";
-      int m_len = matrix.getNumEffects();
-      for(int i=0; i<m_len; i++)
-        {
-        str+=(" "+matrix.getEffect(i).getName()+"("+matrix.getEffect(i).getID()+")" );
-        }
-      str+='\n';
-
-      str+="VERTEX: ";
-      int v_len = vertex.getNumEffects();
-      for(int i=0; i<v_len; i++)
+      switch(mSize)
         {
-        str+=(" "+vertex.getEffect(i).getName()+"("+matrix.getEffect(i).getID()+")" );
+        case 1 : return 1.1f;
+        case 2 : return 1.5f;
+        case 3 : return 1.8f;
+        case 4 : return 2.0f;
+        default: return 3.0f - 4.0f/mSize;
         }
-      str+='\n';
+      }
 
-      str+="FRAGMENT: ";
-      int f_len = fragment.getNumEffects();
-      for(int i=0; i<f_len; i++)
-        {
-        str+=(" "+fragment.getEffect(i).getName()+"("+matrix.getEffect(i).getID()+")" );
-        }
-      str+='\n';
+///////////////////////////////////////////////////////////////////////////////////////////////////
 
-      str+="POSTPROCESS: ";
-      int p_len = postprocess.getNumEffects();
-      for(int i=0; i<p_len; i++)
+    private boolean belongsToRotation(int x, int y, int z, int vector, int row)
+      {
+      switch(vector)
         {
-        str+=(" "+postprocess.getEffect(i).getName()+"("+matrix.getEffect(i).getID()+")" );
+        case VECTX: return mCurrentPosition[x][y][z].get1()==row;
+        case VECTY: return mCurrentPosition[x][y][z].get2()==row;
+        case VECTZ: return mCurrentPosition[x][y][z].get3()==row;
         }
-      str+='\n';
 
-      return str;
+      return false;
       }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public int getNumEffects(EffectType type)
+    private void modifyCurrentPosition(int x, int y, int z, Static4D quat)
       {
-      return mEffects[0][0][0].getNumEffects(type);
+      Static3D current = mCurrentPosition[x][y][z];
+      float diff = 0.5f*(mSize-1);
+      float cubitCenterX = current.get1() - diff;
+      float cubitCenterY = current.get2() - diff;
+      float cubitCenterZ = current.get3() - diff;
+
+      Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
+      Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
+
+      float rotatedX = rotatedCenter.get1() + diff;
+      float rotatedY = rotatedCenter.get2() + diff;
+      float rotatedZ = rotatedCenter.get3() + diff;
+
+      int roundedX = (int)(rotatedX+0.1f);
+      int roundedY = (int)(rotatedY+0.1f);
+      int roundedZ = (int)(rotatedZ+0.1f);
+
+      mCurrentPosition[x][y][z].set1(roundedX);
+      mCurrentPosition[x][y][z].set2(roundedY);
+      mCurrentPosition[x][y][z].set3(roundedZ);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public long addNewRotation(int vector, int row, int angle, long durationMillis, EffectListener listener )
+    boolean isSolved()
       {
-      Static3D axis = VectX;
-      long effectID=0;
-      boolean first = true;
-
-      switch(vector)
-        {
-        case VECTX: axis = VectX; break;
-        case VECTY: axis = VectY; break;
-        case VECTZ: axis = VectZ; break;
-        }
-
-      mRotAxis = vector;
-      mRotRow  = row;
+      Static4D q = mQuatScramble[0][0][0];
 
-      mRotationAngleStatic.set1(0.0f);
+      float x = q.get1();
+      float y = q.get2();
+      float z = q.get3();
+      float w = q.get4();
 
-      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 )
+      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 )
               {
-              if( belongsToRotation(x,y,z,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));
+              q = mQuatScramble[i][j][k];
 
-                if( first )
-                  {
-                  first = false;
-                  effectID = mRotateEffect[x][y][z].getID();
-                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
-                  }
+              if( q.get1()!=x || q.get2()!=y || q.get3()!=z || q.get4()!=w )
+                {
+                return false;
                 }
               }
+            }
 
-      return effectID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getSize()
-      {
-      return mSize;
+      return true;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -415,25 +403,6 @@ public class RubikCube extends DistortedNode
               }
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void continueRotation(float angleInDegrees)
-      {
-      mRotationAngleStatic.set1(angleInDegrees);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private int computeNearestAngle(float angle)
-      {
-      final int NEAREST = 90;
-
-      int tmp = (int)((angle+NEAREST/2)/NEAREST);
-      if( angle< -(NEAREST/2) ) tmp-=1;
-
-      return NEAREST*tmp;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     long finishRotationNow(EffectListener listener)
@@ -484,182 +453,12 @@ public class RubikCube extends DistortedNode
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void removeRotationNow()
+    void continueRotation(float angleInDegrees)
       {
-      float qx=0,qy=0,qz=0;
-      boolean first = true;
-      Static4D quat = null;
-
-      switch(mRotAxis)
-        {
-        case VECTX: qx=1; break;
-        case VECTY: qy=1; break;
-        case VECTZ: qz=1; break;
-        }
+      mRotationAngleStatic.set1(angleInDegrees);
+      }
 
-      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 )
-              {
-              if( belongsToRotation(x,y,z,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).get1();
-                    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;
-                    }
-                  }
-
-                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);
-                }
-              }
-
-      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 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)
-      {
-      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()
-      {
-      switch(mSize)
-        {
-        case 1 : return 1.1f;
-        case 2 : return 1.5f;
-        case 3 : return 1.8f;
-        case 4 : return 2.0f;
-        default: return 3.0f - 4.0f/mSize;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private boolean belongsToRotation(int x, int y, int z, int vector, int row)
-      {
-      switch(vector)
-        {
-        case VECTX: return mCurrentPosition[x][y][z].get1()==row;
-        case VECTY: return mCurrentPosition[x][y][z].get2()==row;
-        case VECTZ: return mCurrentPosition[x][y][z].get3()==row;
-        }
-
-      return false;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void modifyCurrentPosition(int x, int y, int z, Static4D quat)
-      {
-      Static3D current = mCurrentPosition[x][y][z];
-      float diff = 0.5f*(mSize-1);
-      float cubitCenterX = current.get1() - diff;
-      float cubitCenterY = current.get2() - diff;
-      float cubitCenterZ = current.get3() - diff;
-
-      Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
-      Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
-
-      float rotatedX = rotatedCenter.get1() + diff;
-      float rotatedY = rotatedCenter.get2() + diff;
-      float rotatedZ = rotatedCenter.get3() + diff;
-
-      int roundedX = (int)(rotatedX+0.1f);
-      int roundedY = (int)(rotatedY+0.1f);
-      int roundedZ = (int)(rotatedZ+0.1f);
-
-      mCurrentPosition[x][y][z].set1(roundedX);
-      mCurrentPosition[x][y][z].set2(roundedY);
-      mCurrentPosition[x][y][z].set3(roundedZ);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////
 
     void createTexture()
       {
@@ -730,4 +529,162 @@ public class RubikCube extends DistortedNode
       mMove.set( texW*0.5f , texH*0.5f , 0.0f );
       mScale.set(scaleFactor,scaleFactor,scaleFactor);
       }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public Static4D getRotationQuat()
+      {
+      return mQuatAccumulated;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void apply(Effect effect, int position)
+      {
+      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 )
+              {
+              mEffects[x][y][z].apply(effect, position);
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void remove(long effectID)
+      {
+      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 )
+              {
+              mEffects[x][y][z].abortById(effectID);
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void solve()
+      {
+      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 )
+              {
+              mQuatScramble[x][y][z].set(0,0,0,1);
+              mCurrentPosition[x][y][z].set(x,y,z);
+              }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getSize()
+      {
+      return mSize;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public long addNewRotation(int vector, int row, int angle, long durationMillis, EffectListener listener )
+      {
+      Static3D axis = VectX;
+      long effectID=0;
+      boolean first = true;
+
+      switch(vector)
+        {
+        case VECTX: axis = VectX; break;
+        case VECTY: axis = VectY; break;
+        case VECTZ: axis = VectZ; break;
+        }
+
+      mRotAxis = vector;
+      mRotRow  = row;
+
+      mRotationAngleStatic.set1(0.0f);
+
+      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 )
+              {
+              if( belongsToRotation(x,y,z,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));
+
+                if( first )
+                  {
+                  first = false;
+                  effectID = mRotateEffect[x][y][z].getID();
+                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
+                  }
+                }
+              }
+
+      return effectID;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void removeRotationNow()
+      {
+      float qx=0,qy=0,qz=0;
+      boolean first = true;
+      Static4D quat = null;
+
+      switch(mRotAxis)
+        {
+        case VECTX: qx=1; break;
+        case VECTY: qy=1; break;
+        case VECTZ: qz=1; break;
+        }
+
+      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 )
+              {
+              if( belongsToRotation(x,y,z,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).get1();
+                    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;
+                    }
+                  }
+
+                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);
+                }
+              }
+
+      mRotationAngleStatic.set1(0);
+      }
 }
diff --git a/src/main/java/org/distorted/magic/RubikRenderer.java b/src/main/java/org/distorted/magic/RubikRenderer.java
index f77fb873..f46ecdd9 100644
--- a/src/main/java/org/distorted/magic/RubikRenderer.java
+++ b/src/main/java/org/distorted/magic/RubikRenderer.java
@@ -50,6 +50,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
     private boolean mFinishRotation, mRemoveRotation, mSetQuatCurrent, mSetQuatAccumulated;
     private boolean mSizeChangeCube, mSolveCube, mScrambleCube;
     private boolean mCanRotate, mCanDrag, mCanUI;
+    private boolean mIsSolved;
     private RubikCube mOldCube, mNewCube;
     private int mScreenWidth, mScreenHeight;
     private MeshFlat mMesh;
@@ -86,6 +87,144 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
       mNextCubeSize = RubikActivity.getSize();
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private float computeFOV(float cameraDistance, int screenHeight)
+     {
+     double halfFOVInRadians = Math.atan( screenHeight/(2*cameraDistance) );
+     return (float)(2*halfFOVInRadians*(180/Math.PI));
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private void recomputeScaleFactor(int screenWidth, int screenHeight)
+     {
+     mCubeSizeInScreenSpace = CUBE_SCREEN_RATIO*(screenWidth>screenHeight ? screenHeight:screenWidth);
+
+     if( mNewCube!=null )
+       {
+       mNewCube.recomputeScaleFactor(screenWidth, screenHeight, mCubeSizeInScreenSpace);
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private void createCubeNow(int newSize)
+     {
+     if( mOldCube!=null ) mOldCube.releaseResources();
+     mOldCube = mNewCube;
+
+     DistortedTexture texture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
+     DistortedEffects effects = new DistortedEffects();
+
+     mNewCube = new RubikCube(newSize, mView.getQuatCurrent(), mView.getQuatAccumulated(), texture, mMesh, effects);
+     mNewCube.createTexture();
+
+     if( mScreenWidth!=0 )
+       {
+       recomputeScaleFactor(mScreenWidth,mScreenHeight);
+       }
+
+     mIsSolved = true;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// do all 'adjustable' effects (SizeChange, Solve, Scramble)
+
+   private void doEffectNow(BaseEffect.Type type)
+     {
+     int index = type.ordinal();
+
+     mEffectID[index] = type.startEffect(this);
+
+     if( mEffectID[index] < 0 )
+       {
+       mCanUI     = true;
+       mCanRotate = true;
+       mCanDrag   = true;
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// no this will not race with onDrawFrame
+
+   void finishRotation()
+     {
+     mFinishRotation = true;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   boolean createCube(int newSize)
+     {
+     if( mCanDrag && mCanRotate && (mNewCube==null || newSize != mNewCube.getSize()) )
+       {
+       mSizeChangeCube = true;
+       mNextCubeSize = newSize;
+       return true;
+       }
+
+     return false;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   void scrambleCube(int num)
+     {
+     if( mCanUI )
+       {
+       mScrambleCube = true;
+       mScrambleCubeNum = num;
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   void solveCube()
+     {
+     if( mCanUI )
+       {
+       mSolveCube = true;
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   float returnCubeSizeInScreenSpace()
+     {
+     return mCubeSizeInScreenSpace;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   boolean canRotate()
+     {
+     return mCanRotate;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   boolean canDrag()
+     {
+     return mCanDrag;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   void setQuatCurrentOnNextRender()
+     {
+     mSetQuatCurrent = true;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   void setQuatAccumulatedOnNextRender()
+     {
+     mSetQuatAccumulated = true;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // various things are done here delayed, 'after the next render' as not to be done mid-render and
 // cause artifacts.
@@ -120,7 +259,9 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
         mRemoveRotation=false;
         mNewCube.removeRotationNow();
 
-        if( mNewCube.isSolved() )
+        boolean solved = mNewCube.isSolved();
+
+        if( solved && !mIsSolved )
           {
           mCanDrag        = false;
           mCanRotate      = false;
@@ -132,6 +273,8 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
           mCanRotate = true;
           mCanUI     = true;
           }
+
+        mIsSolved = solved;
         }
 
       if( mSizeChangeCube )
@@ -225,126 +368,6 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
        }
      }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private float computeFOV(float cameraDistance, int screenHeight)
-     {
-     double halfFOVInRadians = Math.atan( screenHeight/(2*cameraDistance) );
-     return (float)(2*halfFOVInRadians*(180/Math.PI));
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// no this will not race with onDrawFrame
-
-   void finishRotation()
-     {
-     mFinishRotation = true;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   boolean createCube(int newSize)
-     {
-     if( mCanDrag && mCanRotate && (mNewCube==null || newSize != mNewCube.getSize()) )
-       {
-       mSizeChangeCube = true;
-       mNextCubeSize = newSize;
-       return true;
-       }
-
-     return false;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void recomputeScaleFactor(int screenWidth, int screenHeight)
-     {
-     mCubeSizeInScreenSpace = CUBE_SCREEN_RATIO*(screenWidth>screenHeight ? screenHeight:screenWidth);
-
-     if( mNewCube!=null )
-       {
-       mNewCube.recomputeScaleFactor(screenWidth, screenHeight, mCubeSizeInScreenSpace);
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   void scrambleCube(int num)
-     {
-     if( mCanUI )
-       {
-       mScrambleCube = true;
-       mScrambleCubeNum = num;
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   void solveCube()
-     {
-     if( mCanUI )
-       {
-       mSolveCube = true;
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void createCubeNow(int newSize)
-     {
-     if( mOldCube!=null ) mOldCube.releaseResources();
-     mOldCube = mNewCube;
-
-     DistortedTexture texture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
-     DistortedEffects effects = new DistortedEffects();
-
-     mNewCube = new RubikCube(newSize, mView.getQuatCurrent(), mView.getQuatAccumulated(), texture, mMesh, effects);
-     mNewCube.createTexture();
-
-     if( mScreenWidth!=0 )
-       {
-       recomputeScaleFactor(mScreenWidth,mScreenHeight);
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// do all 'adjustable' effects (SizeChange, Solve, Scramble)
-
-   private void doEffectNow(BaseEffect.Type type)
-     {
-     int index = type.ordinal();
-
-     mEffectID[index] = type.startEffect(this);
-
-     if( mEffectID[index] < 0 )
-       {
-       mCanUI     = true;
-       mCanRotate = true;
-       mCanDrag   = true;
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   float returnCubeSizeInScreenSpace()
-     {
-     return mCubeSizeInScreenSpace;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   boolean canRotate()
-     {
-     return mCanRotate;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   boolean canDrag()
-     {
-     return mCanDrag;
-     }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
    public RubikCube getCube()
@@ -372,18 +395,4 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
      {
      return mScrambleCubeNum;
      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   void setQuatCurrentOnNextRender()
-     {
-     mSetQuatCurrent = true;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   void setQuatAccumulatedOnNextRender()
-     {
-     mSetQuatAccumulated = true;
-     }
 }
diff --git a/src/main/java/org/distorted/magic/RubikSettings.java b/src/main/java/org/distorted/magic/RubikSettings.java
index 21f0aea9..4f164b34 100644
--- a/src/main/java/org/distorted/magic/RubikSettings.java
+++ b/src/main/java/org/distorted/magic/RubikSettings.java
@@ -43,44 +43,6 @@ public class RubikSettings extends AppCompatDialogFragment implements SeekBar.On
   {
   private TextView[] mDurationText;
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public RubikSettings()
-    {
-    mDurationText = new TextView[BaseEffect.Type.LENGTH];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  @NonNull
-  @Override
-  public Dialog onCreateDialog(Bundle savedInstanceState)
-    {
-    FragmentActivity act = getActivity();
-    AlertDialog.Builder builder = new AlertDialog.Builder(act);
-    builder.setCancelable(false);
-
-    LayoutInflater inflater = act.getLayoutInflater();
-    final View view = inflater.inflate(R.layout.settings, null);
-    builder.setView(view);
-
-    LinearLayout linearLayout = view.findViewById(R.id.main_settings_layout);
-
-    if( linearLayout!=null )
-      {
-      for (int i=0; i< BaseEffect.Type.LENGTH; i++)
-        {
-        createSettingsSection(act,linearLayout,i);
-        }
-      }
-    else
-      {
-      android.util.Log.e("settings", "linearLayout NULL!");
-      }
-
-    return builder.create();
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void createSettingsSection(FragmentActivity act, LinearLayout layout, int index)
@@ -88,27 +50,13 @@ public class RubikSettings extends AppCompatDialogFragment implements SeekBar.On
     BaseEffect.Type beType = BaseEffect.Type.getType(index);
     float scale = act.getResources().getDisplayMetrics().density;
 
-    ///// TEXT ///////////////////////////////////////////////////////////////////////////
-
-    int layoutHeight = (int)(scale*48 + 0.5f);
-    int padding      = (int)(scale*10 + 0.5f);
-
-    LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,layoutHeight);
-
-    TextView textView = new TextView(act);
-    textView.setText(beType.getText());
-    textView.setLayoutParams(textParams);
-    textView.setGravity(Gravity.START|Gravity.CENTER);
-    textView.setPadding(padding,0,padding,0);
-    textView.setTextAppearance(android.R.style.TextAppearance_Medium);
-    layout.addView(textView);
-
     ///// OUTER LAYOUT ///////////////////////////////////////////////////////////////////
 
     int margin = (int)(scale*10 + 0.5f);
     int color  = act.getResources().getColor(R.color.grey);
     LinearLayout outerLayout = new LinearLayout(act);
     LinearLayout.LayoutParams outerLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT, 0.5f);
+    outerLayoutParams.topMargin    = margin;
     outerLayoutParams.bottomMargin = margin;
     outerLayoutParams.leftMargin   = margin;
     outerLayoutParams.rightMargin  = margin;
@@ -119,6 +67,21 @@ public class RubikSettings extends AppCompatDialogFragment implements SeekBar.On
     outerLayout.setOrientation(LinearLayout.VERTICAL);
     layout.addView(outerLayout);
 
+    ///// TEXT ///////////////////////////////////////////////////////////////////////////
+
+    int layoutHeight = (int)(scale*32 + 0.5f);
+    int padding      = (int)(scale*10 + 0.5f);
+
+    LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,layoutHeight);
+
+    TextView textView = new TextView(act);
+    textView.setText(beType.getText());
+    textView.setLayoutParams(textParams);
+    textView.setGravity(Gravity.CENTER);
+    textView.setPadding(padding,0,padding,0);
+    textView.setTextAppearance(android.R.style.TextAppearance_Small);
+    outerLayout.addView(textView);
+
     ///// INNER LAYOUT1 //////////////////////////////////////////////////////////////////
 
     int innerLayout1Height = (int)(scale*36 + 0.5f);
@@ -208,6 +171,46 @@ public class RubikSettings extends AppCompatDialogFragment implements SeekBar.On
     spinner.setSelection(beType.getCurrentType());
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public RubikSettings()
+    {
+    mDurationText = new TextView[BaseEffect.Type.LENGTH];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @NonNull
+  @Override
+  public Dialog onCreateDialog(Bundle savedInstanceState)
+    {
+    FragmentActivity act = getActivity();
+    AlertDialog.Builder builder = new AlertDialog.Builder(act);
+    builder.setCancelable(false);
+
+    LayoutInflater inflater = act.getLayoutInflater();
+    final View view = inflater.inflate(R.layout.settings, null);
+    builder.setView(view);
+
+    LinearLayout linearLayout = view.findViewById(R.id.main_settings_layout);
+
+    if( linearLayout!=null )
+      {
+      for (int i=0; i< BaseEffect.Type.LENGTH; i++)
+        {
+        createSettingsSection(act,linearLayout,i);
+        }
+      }
+    else
+      {
+      android.util.Log.e("settings", "linearLayout NULL!");
+      }
+
+    return builder.create();
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void onItemSelected(AdapterView<?> parent, View view, int pos, long id)
diff --git a/src/main/java/org/distorted/magic/RubikSurfaceView.java b/src/main/java/org/distorted/magic/RubikSurfaceView.java
index 13195c99..f0ac93d3 100644
--- a/src/main/java/org/distorted/magic/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/magic/RubikSurfaceView.java
@@ -34,10 +34,10 @@ class RubikSurfaceView extends GLSurfaceView
 {
     // Moving the finger from the middle of the vertical screen to the right edge will rotate a
     // given face by SWIPING_SENSITIVITY/2 degrees.
-    private final static int SWIPING_SENSITIVITY = 240;
+    private final static int SWIPING_SENSITIVITY  = 240;
 
     // Moving the finger by 1/12 the distance of min(scrWidth,scrHeight) will start a Rotation.
-    private final static int ROTATION_SENSITIVITY=  12;
+    private final static int ROTATION_SENSITIVITY =  12;
 
     // Every 1/12 the distance of min(scrWidth,scrHeight) the direction of cube rotation will reset.
     private final static int DIRECTION_SENSITIVITY=  12;
@@ -68,162 +68,58 @@ class RubikSurfaceView extends GLSurfaceView
     private static Static4D mTempAccumulated= new Static4D(0,0,0,1);
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// return quat1*quat2
 
-    public RubikSurfaceView(Context context, AttributeSet attrs)
-      {
-      super(context,attrs);
-
-      if(!isInEditMode())
-        {
-        mRotationVect = VECT[0];
-
-        mPoint = new float[3];
-        mCamera= new float[3];
-        mDiff  = new float[3];
-        mTouchPoint = new float[3];
-        mTouchPointCastOntoFace = new float[3];
-
-        mScreenWidth = mScreenHeight = mScreenMin = 0;
-
-        mRenderer = new RubikRenderer(this);
-
-        final ActivityManager activityManager     = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-        setEGLContextClientVersion( (configurationInfo.reqGlEsVersion>>16) >= 3 ? 3:2 );
-        setRenderer(mRenderer);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event)
-      {
-      int action = event.getAction();
-      int x = (int)event.getX();
-      int y = (int)event.getY();
-
-      switch(action)
-         {
-         case MotionEvent.ACTION_DOWN: mX = x;
-                                       mY = y;
-                                       mLastTouchedFace = faceTouched(x,y);
-
-                                       if( mLastTouchedFace != NONE )
-                                         {
-                                         mDragging           = false;
-                                         mBeginningRotation  = mRenderer.canRotate();
-                                         mContinuingRotation = false;
-                                         }
-                                       else
-                                         {
-                                         mDragging           = mRenderer.canDrag();
-                                         mBeginningRotation  = false;
-                                         mContinuingRotation = false;
-                                         }
-                                       break;
-         case MotionEvent.ACTION_MOVE: if( mDragging )
-                                         {
-                                         mTempCurrent.set(quatFromDrag(mX-x,mY-y));
-                                         mRenderer.setQuatCurrentOnNextRender();
-
-                                         int minimumDist = (mScreenMin*mScreenMin)/(DIRECTION_SENSITIVITY*DIRECTION_SENSITIVITY);
-
-                                         if( (mX-x)*(mX-x) + (mY-y)*(mY-y) > minimumDist )
-                                           {
-                                           mX = x;
-                                           mY = y;
-                                           mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
-                                           mTempCurrent.set(0f, 0f, 0f, 1f);
-                                           mRenderer.setQuatCurrentOnNextRender();
-                                           mRenderer.setQuatAccumulatedOnNextRender();
-                                           }
-                                         }
-                                       if( mBeginningRotation )
-                                         {
-                                         int minimumDistToStartRotating = (mScreenMin*mScreenMin)/(ROTATION_SENSITIVITY*ROTATION_SENSITIVITY);
-
-                                         if( (mX-x)*(mX-x)+(mY-y)*(mY-y) > minimumDistToStartRotating )
-                                           {
-                                           addNewRotation(x,y);
-                                           mBeginningRotation = false;
-                                           mContinuingRotation= true;
-                                           }
-                                         }
-                                       else if( mContinuingRotation )
-                                         {
-                                         continueRotation(x,y);
-                                         }
-                                       break;
-         case MotionEvent.ACTION_UP  : if( mDragging )
-                                         {
-                                         mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
-                                         mTempCurrent.set(0f, 0f, 0f, 1f);
-                                         mRenderer.setQuatCurrentOnNextRender();
-                                         mRenderer.setQuatAccumulatedOnNextRender();
-                                         }
-
-                                       if( mContinuingRotation )
-                                         {
-                                         finishRotation();
-                                         }
-
-                                       break;
-         }
-
-      return true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setQuatAccumulated()
+    static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
       {
-      mQuatAccumulated.set(mTempAccumulated);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      float qx = quat1.get1();
+      float qy = quat1.get2();
+      float qz = quat1.get3();
+      float qw = quat1.get4();
 
-    void setQuatCurrent()
-      {
-      mQuatCurrent.set(mTempCurrent);
-      }
+      float rx = quat2.get1();
+      float ry = quat2.get2();
+      float rz = quat2.get3();
+      float rw = quat2.get4();
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      float tx = rw*qx - rz*qy + ry*qz + rx*qw;
+      float ty = rw*qy + rz*qx + ry*qw - rx*qz;
+      float tz = rw*qz + rz*qw - ry*qx + rx*qy;
+      float tw = rw*qw - rz*qz - ry*qy - rx*qx;
 
-    Static4D getQuatAccumulated()
-      {
-      return mQuatAccumulated;
+      return new Static4D(tx,ty,tz,tw);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
 
-    Static4D getQuatCurrent()
+    static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
       {
-      return mQuatCurrent;
-      }
+      float qx = quat.get1();
+      float qy = quat.get2();
+      float qz = quat.get3();
+      float qw = quat.get4();
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+      Static4D tmp = quatMultiply(quatInverted,vector);
 
-    RubikRenderer getRenderer()
-      {
-      return mRenderer;
+      return quatMultiply(tmp,quat);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
 
-    void setScreenSize(int width, int height)
+    static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
       {
-      mScreenWidth = width;
-      mScreenHeight= height;
-
-      mScreenMin = width<height ? width:height;
-      }
+      float qx = quat.get1();
+      float qy = quat.get2();
+      float qz = quat.get3();
+      float qw = quat.get4();
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+      Static4D tmp = quatMultiply(quat,vector);
 
-    void setCameraDist(float distance)
-      {
-      mCameraDistance = distance;
+      return quatMultiply(tmp,quatInverted);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -310,61 +206,6 @@ class RubikSurfaceView extends GLSurfaceView
       mRenderer.finishRotation();
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return quat1*quat2
-
-    static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
-      {
-      float qx = quat1.get1();
-      float qy = quat1.get2();
-      float qz = quat1.get3();
-      float qw = quat1.get4();
-
-      float rx = quat2.get1();
-      float ry = quat2.get2();
-      float rz = quat2.get3();
-      float rw = quat2.get4();
-
-      float tx = rw*qx - rz*qy + ry*qz + rx*qw;
-      float ty = rw*qy + rz*qx + ry*qw - rx*qz;
-      float tz = rw*qz + rz*qw - ry*qx + rx*qy;
-      float tw = rw*qw - rz*qz - ry*qy - rx*qx;
-
-      return new Static4D(tx,ty,tz,tw);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
-
-    static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
-      {
-      float qx = quat.get1();
-      float qy = quat.get2();
-      float qz = quat.get3();
-      float qw = quat.get4();
-
-      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-      Static4D tmp = quatMultiply(quatInverted,vector);
-
-      return quatMultiply(tmp,quat);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
-
-    static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
-      {
-      float qx = quat.get1();
-      float qy = quat.get2();
-      float qz = quat.get3();
-      float qw = quat.get4();
-
-      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-      Static4D tmp = quatMultiply(quat,vector);
-
-      return quatMultiply(tmp,quatInverted);
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     private Static4D quatFromDrag(float dragX, float dragY)
@@ -513,5 +354,166 @@ class RubikSurfaceView extends GLSurfaceView
 
       return -1;
       }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setQuatAccumulated()
+      {
+      mQuatAccumulated.set(mTempAccumulated);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setQuatCurrent()
+      {
+      mQuatCurrent.set(mTempCurrent);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    Static4D getQuatAccumulated()
+      {
+      return mQuatAccumulated;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    Static4D getQuatCurrent()
+      {
+      return mQuatCurrent;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    RubikRenderer getRenderer()
+      {
+      return mRenderer;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mScreenWidth = width;
+      mScreenHeight= height;
+
+      mScreenMin = width<height ? width:height;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setCameraDist(float distance)
+      {
+      mCameraDistance = distance;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public RubikSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      if(!isInEditMode())
+        {
+        mRotationVect = VECT[0];
+
+        mPoint = new float[3];
+        mCamera= new float[3];
+        mDiff  = new float[3];
+        mTouchPoint = new float[3];
+        mTouchPointCastOntoFace = new float[3];
+
+        mScreenWidth = mScreenHeight = mScreenMin = 0;
+
+        mRenderer = new RubikRenderer(this);
+
+        final ActivityManager activityManager     = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+        setEGLContextClientVersion( (configurationInfo.reqGlEsVersion>>16) >= 3 ? 3:2 );
+        setRenderer(mRenderer);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      int action = event.getAction();
+      int x = (int)event.getX();
+      int y = (int)event.getY();
+
+      switch(action)
+         {
+         case MotionEvent.ACTION_DOWN: mX = x;
+                                       mY = y;
+                                       mLastTouchedFace = faceTouched(x,y);
+
+                                       if( mLastTouchedFace != NONE )
+                                         {
+                                         mDragging           = false;
+                                         mBeginningRotation  = mRenderer.canRotate();
+                                         mContinuingRotation = false;
+                                         }
+                                       else
+                                         {
+                                         mDragging           = mRenderer.canDrag();
+                                         mBeginningRotation  = false;
+                                         mContinuingRotation = false;
+                                         }
+                                       break;
+         case MotionEvent.ACTION_MOVE: if( mDragging )
+                                         {
+                                         mTempCurrent.set(quatFromDrag(mX-x,mY-y));
+                                         mRenderer.setQuatCurrentOnNextRender();
+
+                                         int minimumDist = (mScreenMin*mScreenMin)/(DIRECTION_SENSITIVITY*DIRECTION_SENSITIVITY);
+
+                                         if( (mX-x)*(mX-x) + (mY-y)*(mY-y) > minimumDist )
+                                           {
+                                           mX = x;
+                                           mY = y;
+                                           mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
+                                           mTempCurrent.set(0f, 0f, 0f, 1f);
+                                           mRenderer.setQuatCurrentOnNextRender();
+                                           mRenderer.setQuatAccumulatedOnNextRender();
+                                           }
+                                         }
+                                       if( mBeginningRotation )
+                                         {
+                                         int minimumDistToStartRotating = (mScreenMin*mScreenMin)/(ROTATION_SENSITIVITY*ROTATION_SENSITIVITY);
+
+                                         if( (mX-x)*(mX-x)+(mY-y)*(mY-y) > minimumDistToStartRotating )
+                                           {
+                                           addNewRotation(x,y);
+                                           mBeginningRotation = false;
+                                           mContinuingRotation= true;
+                                           }
+                                         }
+                                       else if( mContinuingRotation )
+                                         {
+                                         continueRotation(x,y);
+                                         }
+                                       break;
+         case MotionEvent.ACTION_UP  : if( mDragging )
+                                         {
+                                         mTempAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
+                                         mTempCurrent.set(0f, 0f, 0f, 1f);
+                                         mRenderer.setQuatCurrentOnNextRender();
+                                         mRenderer.setQuatAccumulatedOnNextRender();
+                                         }
+
+                                       if( mContinuingRotation )
+                                         {
+                                         finishRotation();
+                                         }
+
+                                       break;
+         }
+
+      return true;
+      }
 }
 
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index f33586d2..f44a5b08 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -8,10 +8,10 @@
     <string name="about">About</string>
     <string name="save">SAVE</string>
     <string name="ok">OK</string>
-    <string name="sizechange_effect">Size Change Effect:</string>
-    <string name="solve_effect">Solve Effect:</string>
-    <string name="scramble_effect">Scramble Effect:</string>
-    <string name="win_effect">Win Effect:</string>
+    <string name="sizechange_effect">Size Change Effect</string>
+    <string name="solve_effect">Solve Effect</string>
+    <string name="scramble_effect">Scramble Effect</string>
+    <string name="win_effect">Win Effect</string>
     <string name="duration">Duration:</string>
     <string name="type">Type:</string>
     <string name="credits1">Open Source app developed using the Distorted graphics library. </string>
