commit dd1a65c140e2030d91d8a4351635a14d214d1c5e
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Mon Oct 4 13:39:51 2021 +0200

    Move ObjectControl, the next big chunk of code, to objectlib.

diff --git a/src/main/java/org/distorted/control/RubikControlWhole.java b/src/main/java/org/distorted/control/RubikControlWhole.java
index 3a21092f..6945ba05 100644
--- a/src/main/java/org/distorted/control/RubikControlWhole.java
+++ b/src/main/java/org/distorted/control/RubikControlWhole.java
@@ -281,8 +281,12 @@ class RubikControlWhole
     RubikSurfaceView view = mControl.getSurfaceView();
     float x = point1.get0() + mWidth*0.5f;
     float y = mHeight*0.5f - point1.get1();
+
+    /*
     view.prepareDown();
     view.actionDown(x,y);
+
+     */
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -509,10 +513,14 @@ class RubikControlWhole
     float y2 = mHeight*0.5f - point2s.get1();
 
     RubikSurfaceView view = mControl.getSurfaceView();
+
+    /*
     view.prepareDown();
     view.prepareDown2();
     view.actionDown(x1,y1);
     view.actionDown2(x1,y1,x2,y2);
+
+     */
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -920,7 +928,7 @@ class RubikControlWhole
                 RubikSurfaceView view = mControl.getSurfaceView();
                 float x1 = tmpBuffer[0]+mWidth*0.5f;
                 float y1 = mHeight*0.5f-tmpBuffer[1];
-                view.actionMove(x1,y1,0,0);
+                //view.actionMove(x1,y1,0,0);
                 }
 
               if( finished1 )
@@ -928,8 +936,8 @@ class RubikControlWhole
                 if( mCurrentStage==3 )
                   {
                   RubikSurfaceView view = mControl.getSurfaceView();
-                  view.prepareUp();
-                  view.actionUp();
+                  //view.prepareUp();
+                  //view.actionUp();
                   }
 
                 stageFinished(mCurrentStage);
@@ -947,8 +955,8 @@ class RubikControlWhole
                 float y1 = mHeight*0.5f-tmpBuffer[1];
                 float x2 = tmpBuffer[3]+mWidth*0.5f;
                 float y2 = mHeight*0.5f-tmpBuffer[4];
-                view.prepareMove(x1,y1,x2,y2);
-                view.actionMove(x1,y1,x2,y2);
+                //view.prepareMove(x1,y1,x2,y2);
+                //view.actionMove(x1,y1,x2,y2);
                 }
 
               if( finished2_1 && finished2_2 )
@@ -956,9 +964,9 @@ class RubikControlWhole
                  if( mCurrentStage==11 )
                   {
                   RubikSurfaceView view = mControl.getSurfaceView();
-                  view.prepareUp();
-                  view.actionUp2(true,0,0,false,0,0);
-                  view.actionUp();
+                  //view.prepareUp();
+                  //view.actionUp2(true,0,0,false,0,0);
+                  //view.actionUp();
                   }
 
                 stageFinished(mCurrentStage);
diff --git a/src/main/java/org/distorted/helpers/LockController.java b/src/main/java/org/distorted/helpers/LockController.java
new file mode 100644
index 00000000..f7064783
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/LockController.java
@@ -0,0 +1,188 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube 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.                                                           //
+//                                                                                               //
+// Magic Cube 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 Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.ObjectPreRender;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class LockController
+  {
+  private static final int LOCK_TIME = 300;
+
+  private ObjectPreRender mPre;
+  private ImageButton mLockButton;
+  private long mLockTime;
+  private Timer mTimer;
+  private boolean mTimerRunning;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public LockController()
+    {
+    mTimerRunning= false;
+    mLockTime    = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void toggleLock(TwistyActivity act)
+    {
+    act.toggleLock();
+    mLockButton.setImageResource(getLockIcon(act,false));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getLockIcon(TwistyActivity act, boolean red)
+    {
+    if( act.retLocked() )
+      {
+      if( red )
+        {
+        return RubikActivity.getDrawable(R.drawable.ui_small_locked_red,
+                                         R.drawable.ui_medium_locked_red,
+                                         R.drawable.ui_big_locked_red,
+                                         R.drawable.ui_huge_locked_red);
+        }
+      else
+        {
+        return RubikActivity.getDrawable(R.drawable.ui_small_locked,
+                                         R.drawable.ui_medium_locked,
+                                         R.drawable.ui_big_locked,
+                                         R.drawable.ui_huge_locked);
+        }
+      }
+    else
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,
+                                       R.drawable.ui_medium_unlocked,
+                                       R.drawable.ui_big_unlocked,
+                                       R.drawable.ui_huge_unlocked);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void changeIcon(TwistyActivity act, final boolean red)
+    {
+    act.runOnUiThread(new Runnable()
+      {
+      @Override
+      public void run()
+        {
+        if( mLockButton!=null )
+          mLockButton.setImageResource(getLockIcon(act,red));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reddenLock(final TwistyActivity act)
+    {
+    mLockTime = System.currentTimeMillis();
+
+    if( !mTimerRunning )
+      {
+      changeIcon(act,true);
+
+      mTimerRunning = true;
+      mTimer = new Timer();
+
+      mTimer.scheduleAtFixedRate(new TimerTask()
+        {
+        @Override
+        public void run()
+          {
+          act.runOnUiThread(new Runnable()
+            {
+            @Override
+            public void run()
+              {
+              if( System.currentTimeMillis()-mLockTime > LOCK_TIME )
+                {
+                changeIcon(act,false);
+
+                if( mTimer!=null )
+                  {
+                  mTimer.cancel();
+                  mTimer = null;
+                  }
+
+                mTimerRunning = false;
+                }
+              }
+            });
+          }
+        }, 0, LOCK_TIME);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setupButton(final TwistyActivity act, final float width)
+    {
+    final int icon = getLockIcon(act,false);
+    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mLockButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        toggleLock(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setState(final TwistyActivity act)
+    {
+    act.runOnUiThread(new Runnable()
+      {
+      @Override
+      public void run()
+        {
+        if( mLockButton!=null )
+          mLockButton.setImageResource(getLockIcon(act,false));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ImageButton getButton()
+    {
+    return mLockButton;
+    }
+  }
diff --git a/src/main/java/org/distorted/helpers/MovesAndLockController.java b/src/main/java/org/distorted/helpers/MovesAndLockController.java
deleted file mode 100644
index 73a39a61..00000000
--- a/src/main/java/org/distorted/helpers/MovesAndLockController.java
+++ /dev/null
@@ -1,331 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube 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.                                                           //
-//                                                                                               //
-// Magic Cube 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 Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-import java.util.ArrayList;
-import java.util.Timer;
-import java.util.TimerTask;
-
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-import org.distorted.main.R;
-import org.distorted.main.RubikActivity;
-import org.distorted.objectlib.helpers.BlockController;
-import org.distorted.objectlib.helpers.MovesFinished;
-import org.distorted.objectlib.helpers.TwistyActivity;
-import org.distorted.objectlib.main.ObjectPreRender;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class MovesAndLockController implements MovesFinished
-  {
-  private static final int MILLIS_PER_DEGREE = 6;
-  private static final int LOCK_TIME = 300;
-
-  private static class Move
-    {
-    private final int mAxis, mRow, mAngle;
-
-    Move(int axis, int row, int angle)
-      {
-      mAxis = axis;
-      mRow  = row;
-      mAngle= angle;
-      }
-    }
-
-  private final ArrayList<Move> mMoves;
-  private boolean mCanPrevMove;
-  private ObjectPreRender mPre;
-  private ImageButton mPrevButton, mLockButton;
-  private long mLockTime;
-  private Timer mTimer;
-  private boolean mTimerRunning;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public MovesAndLockController()
-    {
-    mTimerRunning= false;
-    mLockTime    = 0;
-    mCanPrevMove = true;
-    mMoves       = new ArrayList<>();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void toggleLock(TwistyActivity act)
-    {
-    act.toggleLock();
-    mLockButton.setImageResource(getLockIcon(act,false));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getLockIcon(TwistyActivity act, boolean red)
-    {
-    if( act.retLocked() )
-      {
-      if( red )
-        {
-        return RubikActivity.getDrawable(R.drawable.ui_small_locked_red,
-                                         R.drawable.ui_medium_locked_red,
-                                         R.drawable.ui_big_locked_red,
-                                         R.drawable.ui_huge_locked_red);
-        }
-      else
-        {
-        return RubikActivity.getDrawable(R.drawable.ui_small_locked,
-                                         R.drawable.ui_medium_locked,
-                                         R.drawable.ui_big_locked,
-                                         R.drawable.ui_huge_locked);
-        }
-      }
-    else
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,
-                                       R.drawable.ui_medium_unlocked,
-                                       R.drawable.ui_big_unlocked,
-                                       R.drawable.ui_huge_unlocked);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getPrevIcon(boolean on)
-    {
-    if( on )
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_cube_back,
-                                       R.drawable.ui_medium_cube_back,
-                                       R.drawable.ui_big_cube_back,
-                                       R.drawable.ui_huge_cube_back);
-      }
-    else
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_cube_grey,
-                                       R.drawable.ui_medium_cube_grey,
-                                       R.drawable.ui_big_cube_grey,
-                                       R.drawable.ui_huge_cube_grey);
-      }
-    }
-
-//////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void backMove(TwistyActivity act)
-    {
-    if( mCanPrevMove )
-      {
-      int numMoves = mMoves.size();
-
-      if( numMoves>0 )
-        {
-        Move move   = mMoves.remove(numMoves-1);
-        int axis    = move.mAxis;
-        int row     = (1<<move.mRow);
-        int angle   = move.mAngle;
-        int duration= Math.abs(angle)*MILLIS_PER_DEGREE;
-
-        if( angle!=0 )
-          {
-          mCanPrevMove = false;
-          mPre = act.getPreRender();
-          mPre.blockTouch(BlockController.MOVES_PLACE_0);
-          mPre.addRotation(this, axis, row, -angle, duration);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: trying to back move of angle 0");
-          }
-
-        if( numMoves==1 ) changeBackMove(act, false);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void changeBackMove(TwistyActivity act, final boolean on)
-    {
-    act.runOnUiThread(new Runnable()
-      {
-      @Override
-      public void run()
-        {
-        if( mPrevButton!=null )
-          mPrevButton.setImageResource(getPrevIcon(on));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void changeLockIcon(TwistyActivity act, final boolean red)
-    {
-    act.runOnUiThread(new Runnable()
-      {
-      @Override
-      public void run()
-        {
-        if( mLockButton!=null )
-          mLockButton.setImageResource(getLockIcon(act,red));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reddenLock(final TwistyActivity act)
-    {
-    mLockTime = System.currentTimeMillis();
-
-    if( !mTimerRunning )
-      {
-      changeLockIcon(act,true);
-
-      mTimerRunning = true;
-      mTimer = new Timer();
-
-      mTimer.scheduleAtFixedRate(new TimerTask()
-        {
-        @Override
-        public void run()
-          {
-          act.runOnUiThread(new Runnable()
-            {
-            @Override
-            public void run()
-              {
-              if( System.currentTimeMillis()-mLockTime > LOCK_TIME )
-                {
-                changeLockIcon(act,false);
-
-                if( mTimer!=null )
-                  {
-                  mTimer.cancel();
-                  mTimer = null;
-                  }
-
-                mTimerRunning = false;
-                }
-              }
-            });
-          }
-        }, 0, LOCK_TIME);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void addMove(TwistyActivity act, int axis, int row, int angle)
-    {
-    if( mMoves.isEmpty() ) changeBackMove(act,true);
-    mMoves.add(new Move(axis,row,angle));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onActionFinished(final long effectID)
-    {
-    mCanPrevMove = true;
-    mPre.unblockTouch();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void clearMoves(final TwistyActivity act)
-    {
-    mMoves.clear();
-    changeBackMove(act,false);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumMoves()
-    {
-    return mMoves.size();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setupPrevButton(final TwistyActivity act, final float width)
-    {
-    final int icon = getPrevIcon( !mMoves.isEmpty() );
-    mPrevButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        backMove(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setupLockButton(final TwistyActivity act, final float width)
-    {
-    final int icon = getLockIcon(act,false);
-    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mLockButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        toggleLock(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setLockState(final TwistyActivity act)
-    {
-    act.runOnUiThread(new Runnable()
-      {
-      @Override
-      public void run()
-        {
-        if( mLockButton!=null )
-          mLockButton.setImageResource(getLockIcon(act,false));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ImageButton getPrevButton()
-    {
-    return mPrevButton;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ImageButton getLockButton()
-    {
-    return mLockButton;
-    }
-  }
diff --git a/src/main/java/org/distorted/helpers/MovesController.java b/src/main/java/org/distorted/helpers/MovesController.java
new file mode 100644
index 00000000..1227ee41
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/MovesController.java
@@ -0,0 +1,190 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube 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.                                                           //
+//                                                                                               //
+// Magic Cube 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 Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import java.util.ArrayList;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.MovesFinished;
+import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.ObjectPreRender;
+
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class MovesController implements MovesFinished
+  {
+  private static final int MILLIS_PER_DEGREE = 6;
+
+  private static class Move
+    {
+    private final int mAxis, mRow, mAngle;
+
+    Move(int axis, int row, int angle)
+      {
+      mAxis = axis;
+      mRow  = row;
+      mAngle= angle;
+      }
+    }
+
+  private final ArrayList<Move> mMoves;
+  private boolean mCanPrevMove;
+  private ObjectPreRender mPre;
+  private ImageButton mPrevButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public MovesController()
+    {
+    mCanPrevMove = true;
+    mMoves       = new ArrayList<>();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getPrevIcon(boolean on)
+    {
+    if( on )
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_cube_back,
+                                       R.drawable.ui_medium_cube_back,
+                                       R.drawable.ui_big_cube_back,
+                                       R.drawable.ui_huge_cube_back);
+      }
+    else
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_cube_grey,
+                                       R.drawable.ui_medium_cube_grey,
+                                       R.drawable.ui_big_cube_grey,
+                                       R.drawable.ui_huge_cube_grey);
+      }
+    }
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void backMove(TwistyActivity act)
+    {
+    if( mCanPrevMove )
+      {
+      int numMoves = mMoves.size();
+
+      if( numMoves>0 )
+        {
+        Move move   = mMoves.remove(numMoves-1);
+        int axis    = move.mAxis;
+        int row     = (1<<move.mRow);
+        int angle   = move.mAngle;
+        int duration= Math.abs(angle)*MILLIS_PER_DEGREE;
+
+        if( angle!=0 )
+          {
+          mCanPrevMove = false;
+          mPre = act.getPreRender();
+          mPre.blockTouch(BlockController.MOVES_PLACE_0);
+          mPre.addRotation(this, axis, row, -angle, duration);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: trying to back move of angle 0");
+          }
+
+        if( numMoves==1 ) changeBackMove(act, false);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void changeBackMove(TwistyActivity act, final boolean on)
+    {
+    act.runOnUiThread(new Runnable()
+      {
+      @Override
+      public void run()
+        {
+        if( mPrevButton!=null )
+          mPrevButton.setImageResource(getPrevIcon(on));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addMove(TwistyActivity act, int axis, int row, int angle)
+    {
+    if( mMoves.isEmpty() ) changeBackMove(act,true);
+    mMoves.add(new Move(axis,row,angle));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onActionFinished(final long effectID)
+    {
+    mCanPrevMove = true;
+    mPre.unblockTouch();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void clearMoves(final TwistyActivity act)
+    {
+    mMoves.clear();
+    changeBackMove(act,false);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumMoves()
+    {
+    return mMoves.size();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setupButton(final TwistyActivity act, final float width)
+    {
+    final int icon = getPrevIcon( !mMoves.isEmpty() );
+    mPrevButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        backMove(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ImageButton getButton()
+    {
+    return mPrevButton;
+    }
+  }
diff --git a/src/main/java/org/distorted/main/RubikObjectStateActioner.java b/src/main/java/org/distorted/main/RubikObjectStateActioner.java
index a6aea001..1b411225 100644
--- a/src/main/java/org/distorted/main/RubikObjectStateActioner.java
+++ b/src/main/java/org/distorted/main/RubikObjectStateActioner.java
@@ -39,8 +39,11 @@ import org.distorted.dialogs.RubikDialogNewRecord;
 import org.distorted.dialogs.RubikDialogSolved;
 import org.distorted.network.RubikScores;
 import org.distorted.screens.RubikScreenPlay;
+import org.distorted.screens.RubikScreenReady;
+import org.distorted.screens.RubikScreenSolver;
 import org.distorted.screens.RubikScreenSolving;
 import org.distorted.screens.ScreenList;
+import org.distorted.solvers.SolverMain;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -217,6 +220,66 @@ public class RubikObjectStateActioner implements ObjectStateActioner
        });
      }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void onFinishRotation(TwistyActivity act, int axis, int row, int angle)
+     {
+     if( ScreenList.getCurrentScreen()== ScreenList.SOLV )
+       {
+       RubikScreenSolving solv = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
+       solv.addMove(act, axis, row, angle);
+       }
+     if( ScreenList.getCurrentScreen()== ScreenList.PLAY )
+       {
+       RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
+       play.addMove(act, axis, row, angle);
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void onBeginRotation(TwistyActivity act)
+     {
+     if( ScreenList.getCurrentScreen()== ScreenList.READ )
+       {
+       RubikScreenSolving solving = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
+       solving.resetElapsed();
+       RubikActivity ract = (RubikActivity)act;
+
+       ract.runOnUiThread(new Runnable()
+         {
+         @Override
+         public void run()
+           {
+           ScreenList.switchScreen( ract, ScreenList.SOLV);
+           }
+         });
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void failedToDrag(TwistyActivity act)
+      {
+      ScreenList curr = ScreenList.getCurrentScreen();
+
+      if( curr==ScreenList.PLAY )
+        {
+        RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
+        play.reddenLock(act);
+        }
+      else if( curr==ScreenList.READ )
+        {
+        RubikScreenReady read = (RubikScreenReady) ScreenList.READ.getScreenClass();
+        read.reddenLock(act);
+        }
+      else if( curr==ScreenList.SOLV )
+        {
+        RubikScreenSolving solv = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
+        solv.reddenLock(act);
+        }
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
    public void onSolved()
@@ -237,4 +300,19 @@ public class RubikObjectStateActioner implements ObjectStateActioner
           }
         }
      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public int getCurrentColor()
+     {
+     RubikScreenSolver solver = (RubikScreenSolver) ScreenList.SVER.getScreenClass();
+     return solver.getCurrentColor();
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public int cubitIsLocked(ObjectType type, int cubit)
+     {
+     return SolverMain.cubitIsLocked(type,cubit);
+     }
 }
diff --git a/src/main/java/org/distorted/main/RubikSurfaceView.java b/src/main/java/org/distorted/main/RubikSurfaceView.java
index 3d1b0ac7..bf98653f 100644
--- a/src/main/java/org/distorted/main/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/main/RubikSurfaceView.java
@@ -25,73 +25,28 @@ import android.content.pm.ConfigurationInfo;
 import android.opengl.GLES30;
 import android.opengl.GLSurfaceView;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.view.MotionEvent;
 
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
 import org.distorted.library.main.DistortedScreen;
-import org.distorted.library.type.Static2D;
 import org.distorted.library.type.Static4D;
-import org.distorted.library.main.QuatHelper;
 
 import org.distorted.objectlib.helpers.ObjectSurfaceView;
 import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.ObjectControl;
 import org.distorted.objectlib.main.ObjectPreRender;
-import org.distorted.objectlib.main.TwistyObject;
 import org.distorted.objectlib.main.Movement;
 
-import org.distorted.screens.RubikScreenReady;
 import org.distorted.screens.ScreenList;
-import org.distorted.screens.RubikScreenPlay;
-import org.distorted.screens.RubikScreenSolver;
-import org.distorted.screens.RubikScreenSolving;
-import org.distorted.solvers.SolverMain;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
 {
-    public static final int NUM_SPEED_PROBES = 10;
-    public static final int INVALID_POINTER_ID = -1;
-
-    public static final int MODE_ROTATE  = 0;
-    public static final int MODE_DRAG    = 1;
-    public static final int MODE_REPLACE = 2;
-
-    // Moving the finger from the middle of the vertical screen to the right edge will rotate a
-    // given face by SWIPING_SENSITIVITY/2 degrees.
-    public final static int SWIPING_SENSITIVITY  = 240;
-    // Moving the finger by 0.3 of an inch will start a Rotation.
-    public final static float ROTATION_SENSITIVITY = 0.3f;
-
-    private final Static4D CAMERA_POINT = new Static4D(0, 0, 0, 0);
-
+    private ObjectControl mObjectController;
     private RubikRenderer mRenderer;
-    private ObjectPreRender mPreRender;
-    private Movement mMovement;
-    private boolean mDragging, mBeginningRotation, mContinuingRotation;
-    private int mScreenWidth, mScreenHeight, mScreenMin;
-
-    private float mRotAngle, mInitDistance;
-    private float mStartRotX, mStartRotY;
-    private float mAxisX, mAxisY;
-    private float mRotationFactor;
-    private int mLastCubitColor, mLastCubitFace, mLastCubit;
-    private int mCurrentAxis, mCurrentRow;
-    private float mCurrentAngle, mCurrRotSpeed;
-    private float[] mLastX;
-    private float[] mLastY;
-    private long[] mLastT;
-    private int mFirstIndex, mLastIndex;
-    private int mDensity;
-
-    private int mPointer1, mPointer2;
-    private float mX1, mY1, mX2, mY2, mX, mY;
-    private boolean mIsAutomatic;
-
-    private static final Static4D mQuat= new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
-    private static final Static4D mTemp= new Static4D(0,0,0,1);
+    private int mScreenWidth, mScreenHeight;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -99,8 +54,7 @@ public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
       {
       mScreenWidth = width;
       mScreenHeight= height;
-
-      mScreenMin = Math.min(width, height);
+      mObjectController.setScreenSize(width,height);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -121,379 +75,7 @@ public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
 
     ObjectPreRender getPreRender()
       {
-      return mPreRender;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// cast the 3D axis we are currently rotating along (which is already casted to the surface of the
-// currently touched face AND converted into a 4D vector - fourth 0) to a 2D in-screen-surface axis
-
-    private void computeCurrentAxis(Static4D axis)
-      {
-      Static4D result = QuatHelper.rotateVectorByQuat(axis, mQuat);
-
-      mAxisX =result.get0();
-      mAxisY =result.get1();
-
-      float len = (float)Math.sqrt(mAxisX*mAxisX + mAxisY*mAxisY);
-      mAxisX /= len;
-      mAxisY /= len;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void addSpeedProbe(float x, float y)
-      {
-      long currTime = System.currentTimeMillis();
-      boolean theSame = mLastIndex==mFirstIndex;
-
-      mLastIndex++;
-      if( mLastIndex>=NUM_SPEED_PROBES ) mLastIndex=0;
-
-      mLastT[mLastIndex] = currTime;
-      mLastX[mLastIndex] = x;
-      mLastY[mLastIndex] = y;
-
-      if( mLastIndex==mFirstIndex)
-        {
-        mFirstIndex++;
-        if( mFirstIndex>=NUM_SPEED_PROBES ) mFirstIndex=0;
-        }
-
-      if( theSame )
-        {
-        mLastT[mFirstIndex] = currTime;
-        mLastX[mFirstIndex] = x;
-        mLastY[mFirstIndex] = y;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void computeCurrentSpeedInInchesPerSecond()
-      {
-      long firstTime = mLastT[mFirstIndex];
-      long lastTime  = mLastT[mLastIndex];
-      float fX = mLastX[mFirstIndex];
-      float fY = mLastY[mFirstIndex];
-      float lX = mLastX[mLastIndex];
-      float lY = mLastY[mLastIndex];
-
-      long timeDiff = lastTime-firstTime;
-
-      mLastIndex = 0;
-      mFirstIndex= 0;
-
-      mCurrRotSpeed = timeDiff>0 ? 1000*retFingerDragDistanceInInches(fX,fY,lX,lY)/timeDiff : 0;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float retFingerDragDistanceInInches(float xFrom, float yFrom, float xTo, float yTo)
-      {
-      float xDist = mScreenWidth*(xFrom-xTo);
-      float yDist = mScreenHeight*(yFrom-yTo);
-      float distInPixels = (float)Math.sqrt(xDist*xDist + yDist*yDist);
-
-      return distInPixels/mDensity;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void setUpDragOrRotate(boolean down, float x, float y)
-      {
-      int mode = ScreenList.getMode();
-
-      if( mode==MODE_DRAG )
-        {
-        mDragging           = true;
-        mBeginningRotation  = false;
-        mContinuingRotation = false;
-        }
-      else
-        {
-        TwistyObject object = mPreRender.getObject();
-        CAMERA_POINT.set2( object==null ? 1.21f : object.getCameraDist() );
-
-        Static4D touchPoint = new Static4D(x, y, 0, 0);
-        Static4D rotatedTouchPoint= QuatHelper.rotateVectorByInvertedQuat(touchPoint, mQuat);
-        Static4D rotatedCamera= QuatHelper.rotateVectorByInvertedQuat(CAMERA_POINT, mQuat);
-
-        if( object!=null && mMovement!=null && mMovement.faceTouched(rotatedTouchPoint,rotatedCamera,object.getObjectRatio() ) )
-          {
-          mDragging           = false;
-          mContinuingRotation = false;
-
-          if( mode==MODE_ROTATE )
-            {
-            mBeginningRotation= !mPreRender.isTouchBlocked();
-            }
-          else if( mode==MODE_REPLACE )
-            {
-            mBeginningRotation= false;
-
-            if( down )
-              {
-              RubikScreenSolver solver = (RubikScreenSolver) ScreenList.SVER.getScreenClass();
-              mLastCubitFace = mMovement.getTouchedFace();
-              float[] point = mMovement.getTouchedPoint3D();
-              int color = solver.getCurrentColor();
-              mLastCubit = object.getCubit(point);
-              mPreRender.setTextureMap( mLastCubit, mLastCubitFace, color );
-              mLastCubitColor = SolverMain.cubitIsLocked(object.getObjectType(), mLastCubit);
-              }
-            }
-          }
-        else
-          {
-          final RubikActivity act = (RubikActivity)getContext();
-          final boolean locked= act.isLocked();
-          mDragging           = (!locked || mIsAutomatic);
-          mBeginningRotation  = false;
-          mContinuingRotation = false;
-          if( !mDragging ) reddenLockIcon(act);
-          }
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void reddenLockIcon(RubikActivity act)
-      {
-      ScreenList curr = ScreenList.getCurrentScreen();
-
-      if( curr==ScreenList.PLAY )
-        {
-        RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
-        play.reddenLock(act);
-        }
-      else if( curr==ScreenList.READ )
-        {
-        RubikScreenReady read = (RubikScreenReady) ScreenList.READ.getScreenClass();
-        read.reddenLock(act);
-        }
-      else if( curr==ScreenList.SOLV )
-        {
-        RubikScreenSolving solv = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
-        solv.reddenLock(act);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void drag(float x, float y)
-      {
-      if( mPointer1!=INVALID_POINTER_ID && mPointer2!=INVALID_POINTER_ID)
-        {
-        float x2 = (mX2 - mScreenWidth*0.5f)/mScreenMin;
-        float y2 = (mScreenHeight*0.5f - mY2)/mScreenMin;
-
-        float angleNow = getAngle(x,y,x2,y2);
-        float angleDiff = angleNow-mRotAngle;
-        float sinA =-(float)Math.sin(angleDiff);
-        float cosA = (float)Math.cos(angleDiff);
-
-        Static4D dragQuat = QuatHelper.quatMultiply(new Static4D(0,0,sinA,cosA), mQuat);
-        mTemp.set(dragQuat);
-
-        mRotAngle = angleNow;
-
-        float distNow  = (float)Math.sqrt( (x-x2)*(x-x2) + (y-y2)*(y-y2) );
-        float distQuot = mInitDistance<0 ? 1.0f : distNow/ mInitDistance;
-        mInitDistance = distNow;
-        TwistyObject object = mPreRender.getObject();
-        if( object!=null ) object.setObjectRatio(distQuot);
-        }
-      else
-        {
-        Static4D dragQuat = QuatHelper.quatMultiply(QuatHelper.quatFromDrag(mX-x,y-mY), mQuat);
-        mTemp.set(dragQuat);
-        }
-
-      mPreRender.setQuatOnNextRender();
-      mX = x;
-      mY = y;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void finishRotation()
-      {
-      computeCurrentSpeedInInchesPerSecond();
-      int angle = mPreRender.getObject().computeNearestAngle(mCurrentAxis,mCurrentAngle, mCurrRotSpeed);
-      mPreRender.finishRotation(angle);
-      mPreRender.rememberMove(mCurrentAxis,mCurrentRow,angle);
-
-      if( angle!=0 )
-        {
-        if( ScreenList.getCurrentScreen()== ScreenList.SOLV )
-          {
-          RubikActivity act = (RubikActivity)getContext();
-          RubikScreenSolving solving = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
-          solving.addMove(act, mCurrentAxis, mCurrentRow, angle);
-          }
-        if( ScreenList.getCurrentScreen()== ScreenList.PLAY )
-          {
-          RubikActivity act = (RubikActivity)getContext();
-          RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
-          play.addMove(act, mCurrentAxis, mCurrentRow, angle);
-          }
-        }
-
-      mContinuingRotation = false;
-      mBeginningRotation  = false;
-      mDragging           = true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void continueRotation(float x, float y)
-      {
-      float dx = x-mStartRotX;
-      float dy = y-mStartRotY;
-      float alpha = dx*mAxisX + dy*mAxisY;
-      float x2 = dx - alpha*mAxisX;
-      float y2 = dy - alpha*mAxisY;
-
-      float len = (float)Math.sqrt(x2*x2 + y2*y2);
-
-      // we have the length of 1D vector 'angle', now the direction:
-      float tmp = mAxisY==0 ? -mAxisX*y2 : mAxisY*x2;
-
-      float angle = (tmp>0 ? 1:-1)*len*mRotationFactor;
-      mCurrentAngle = SWIPING_SENSITIVITY*angle;
-      mPreRender.getObject().continueRotation(mCurrentAngle);
-
-      addSpeedProbe(x2,y2);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void beginRotation(float x, float y)
-      {
-      mStartRotX = x;
-      mStartRotY = y;
-
-      TwistyObject object = mPreRender.getObject();
-      int numLayers = object.getNumLayers();
-
-      Static4D touchPoint2 = new Static4D(x, y, 0, 0);
-      Static4D rotatedTouchPoint2= QuatHelper.rotateVectorByInvertedQuat(touchPoint2, mQuat);
-      Static2D res = mMovement.newRotation(rotatedTouchPoint2,object.getObjectRatio());
-
-      mCurrentAxis = (int)res.get0();
-      mCurrentRow  = (int)res.get1();
-
-      computeCurrentAxis( mMovement.getCastedRotAxis(mCurrentAxis) );
-      mRotationFactor = mMovement.returnRotationFactor(numLayers,mCurrentRow);
-
-      object.beginNewRotation( mCurrentAxis, mCurrentRow );
-
-      if( ScreenList.getCurrentScreen()== ScreenList.READ )
-        {
-        RubikScreenSolving solving = (RubikScreenSolving) ScreenList.SOLV.getScreenClass();
-        solving.resetElapsed();
-
-        final RubikActivity act = (RubikActivity)getContext();
-
-        act.runOnUiThread(new Runnable()
-          {
-          @Override
-          public void run()
-            {
-            ScreenList.switchScreen( act, ScreenList.SOLV);
-            }
-          });
-        }
-
-      addSpeedProbe(x,y);
-
-      mBeginningRotation = false;
-      mContinuingRotation= true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float getAngle(float x1, float y1, float x2, float y2)
-      {
-      return (float) Math.atan2(y1-y2, x1-x2);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void initialize()
-      {
-      mPointer1 = INVALID_POINTER_ID;
-      mPointer2 = INVALID_POINTER_ID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void prepareDown(MotionEvent event)
-      {
-      mPointer1 = event.getPointerId(0);
-      mX1 = event.getX();
-      mY1 = event.getY();
-      mPointer2 = INVALID_POINTER_ID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void prepareMove(MotionEvent event)
-      {
-      int index1 = event.findPointerIndex(mPointer1);
-
-      if( index1>=0 )
-        {
-        mX1 = event.getX(index1);
-        mY1 = event.getY(index1);
-        }
-
-      int index2 = event.findPointerIndex(mPointer2);
-
-      if( index2>=0 )
-        {
-        mX2 = event.getX(index2);
-        mY2 = event.getY(index2);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void prepareUp(MotionEvent event)
-      {
-      mPointer1 = INVALID_POINTER_ID;
-      mPointer2 = INVALID_POINTER_ID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void prepareDown2(MotionEvent event)
-      {
-      int index = event.getActionIndex();
-
-      if( mPointer1==INVALID_POINTER_ID )
-        {
-        mPointer1 = event.getPointerId(index);
-        mX1 = event.getX(index);
-        mY1 = event.getY(index);
-        }
-      else if( mPointer2==INVALID_POINTER_ID )
-        {
-        mPointer2 = event.getPointerId(index);
-        mX2 = event.getX(index);
-        mY2 = event.getY(index);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void prepareUp2(MotionEvent event)
-      {
-      int index = event.getActionIndex();
-
-           if( index==event.findPointerIndex(mPointer1) ) mPointer1 = INVALID_POINTER_ID;
-      else if( index==event.findPointerIndex(mPointer2) ) mPointer2 = INVALID_POINTER_ID;
+      return mObjectController.getPreRender();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -506,25 +88,9 @@ public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
 
       if(!isInEditMode())
         {
-        mIsAutomatic = false;
-
-        mLastCubitColor = -1;
-        mCurrRotSpeed   = 0.0f;
-
-        mLastX = new float[NUM_SPEED_PROBES];
-        mLastY = new float[NUM_SPEED_PROBES];
-        mLastT = new long[NUM_SPEED_PROBES];
-        mFirstIndex =0;
-        mLastIndex  =0;
-
-        mRenderer  = new RubikRenderer(this);
-        mPreRender = new ObjectPreRender(this,new RubikObjectStateActioner());
-
         RubikActivity act = (RubikActivity)context;
-        DisplayMetrics dm = new DisplayMetrics();
-        act.getWindowManager().getDefaultDisplay().getMetrics(dm);
-
-        mDensity = dm.densityDpi;
+        mRenderer = new RubikRenderer(this);
+        mObjectController = new ObjectControl(act,this,new RubikObjectStateActioner());
 
         final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
 
@@ -556,23 +122,23 @@ public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void setQuat()
+    public void setMovement(Movement movement)
       {
-      mQuat.set(mTemp);
+      mObjectController.setMovement(movement);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public Static4D getQuat()
+    public void setQuat()
       {
-      return mQuat;
+      mObjectController.setQuat();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void setMovement(Movement movement)
+    public Static4D getQuat()
       {
-      mMovement = movement;
+      return mObjectController.getQuat();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -591,132 +157,9 @@ public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void prepareDown()
+    public void initialize()
       {
-      mIsAutomatic = true;
-      mPointer1 = 0;
-      mPointer2 = INVALID_POINTER_ID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void prepareDown2()
-      {
-      mPointer2 = 0;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void prepareMove(float x1, float y1, float x2, float y2)
-      {
-      mX1 = x1;
-      mY1 = y1;
-      mX2 = x2;
-      mY2 = y2;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void prepareUp()
-      {
-      mIsAutomatic = false;
-      mPointer1 = INVALID_POINTER_ID;
-      mPointer2 = INVALID_POINTER_ID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void actionMove(float x1, float y1, float x2, float y2)
-      {
-      float pX = mPointer1 != INVALID_POINTER_ID ? x1 : x2;
-      float pY = mPointer1 != INVALID_POINTER_ID ? y1 : y2;
-
-      float x = (pX - mScreenWidth*0.5f)/mScreenMin;
-      float y = (mScreenHeight*0.5f -pY)/mScreenMin;
-
-      if( mBeginningRotation )
-        {
-        if( retFingerDragDistanceInInches(mX,mY,x,y) > ROTATION_SENSITIVITY )
-          {
-          beginRotation(x,y);
-          }
-        }
-      else if( mContinuingRotation )
-        {
-        continueRotation(x,y);
-        }
-      else if( mDragging )
-        {
-        drag(x,y);
-        }
-      else
-        {
-        setUpDragOrRotate(false,x,y);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void actionDown(float x, float y)
-      {
-      mX = (x -  mScreenWidth*0.5f)/mScreenMin;
-      mY = (mScreenHeight*0.5f - y)/mScreenMin;
-
-      setUpDragOrRotate(true,mX,mY);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void actionUp()
-      {
-      if( mContinuingRotation )
-        {
-        finishRotation();
-        }
-
-      if( mLastCubitColor>=0 )
-        {
-        mPreRender.setTextureMap( mLastCubit, mLastCubitFace, mLastCubitColor );
-        mLastCubitColor = -1;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void actionDown2(float x1, float y1, float x2, float y2)
-      {
-      mRotAngle = getAngle(x1,-y1, x2,-y2);
-      mInitDistance = -1;
-
-      mX = (x1 - mScreenWidth*0.5f )/mScreenMin;
-      mY = (mScreenHeight*0.5f - y1)/mScreenMin;
-
-      if( mBeginningRotation )
-        {
-        mContinuingRotation = false;
-        mBeginningRotation  = false;
-        mDragging           = true;
-        }
-      else if( mContinuingRotation )
-        {
-        finishRotation();
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void actionUp2(boolean p1isUp, float x1, float y1, boolean p2isUp, float x2, float y2)
-      {
-      if( p1isUp )
-        {
-        mX = (x2 -  mScreenWidth*0.5f)/mScreenMin;
-        mY = (mScreenHeight*0.5f - y2)/mScreenMin;
-        }
-      if( p2isUp )
-        {
-        mX = (x1 -  mScreenWidth*0.5f)/mScreenMin;
-        mY = (mScreenHeight*0.5f - y1)/mScreenMin;
-        }
+      mObjectController.initialize();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -724,30 +167,8 @@ public class RubikSurfaceView extends GLSurfaceView implements ObjectSurfaceView
     @Override
     public boolean onTouchEvent(MotionEvent event)
       {
-      int action = event.getActionMasked();
-
-      switch(action)
-         {
-         case MotionEvent.ACTION_DOWN        : prepareDown(event);
-                                               actionDown(mX1, mY1);
-                                               break;
-         case MotionEvent.ACTION_MOVE        : prepareMove(event);
-                                               actionMove(mX1, mY1, mX2, mY2);
-                                               break;
-         case MotionEvent.ACTION_UP          : prepareUp(event);
-                                               actionUp();
-                                               break;
-         case MotionEvent.ACTION_POINTER_DOWN: prepareDown2(event);
-                                               actionDown2(mX1, mY1, mX2, mY2);
-                                               break;
-         case MotionEvent.ACTION_POINTER_UP  : prepareUp2(event);
-                                               boolean p1isUp = mPointer1==INVALID_POINTER_ID;
-                                               boolean p2isUp = mPointer2==INVALID_POINTER_ID;
-                                               actionUp2(p1isUp, mX1, mY1, p2isUp, mX2, mY2);
-                                               break;
-         }
-
-      return true;
+      int mode = ScreenList.getMode();
+      return mObjectController.onTouchEvent(event,mode);
       }
 }
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenBase.java b/src/main/java/org/distorted/screens/RubikScreenBase.java
index 19c6ba64..234875c1 100644
--- a/src/main/java/org/distorted/screens/RubikScreenBase.java
+++ b/src/main/java/org/distorted/screens/RubikScreenBase.java
@@ -22,7 +22,8 @@ package org.distorted.screens;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 
-import org.distorted.helpers.MovesAndLockController;
+import org.distorted.helpers.LockController;
+import org.distorted.helpers.MovesController;
 import org.distorted.objectlib.helpers.TwistyActivity;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
@@ -31,13 +32,14 @@ import org.distorted.main.RubikActivity;
 
 abstract class RubikScreenBase extends RubikScreenAbstract
   {
-  MovesAndLockController mController;
+  private final LockController mLockController;
+  protected MovesController mMovesController;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   void createBottomPane(final RubikActivity act, float width, ImageButton button)
     {
-    mController.clearMoves(act);
+    mMovesController.clearMoves(act);
 
     LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
     layoutBot.removeAllViews();
@@ -51,10 +53,10 @@ abstract class RubikScreenBase extends RubikScreenAbstract
     LinearLayout layoutRight = new LinearLayout(act);
     layoutRight.setLayoutParams(params);
 
-    mController.setupPrevButton(act,width);
-    layoutLeft.addView(mController.getPrevButton());
-    mController.setupLockButton(act,width);
-    layoutMid.addView(mController.getLockButton());
+    mMovesController.setupButton(act,width);
+    layoutLeft.addView(mMovesController.getButton());
+    mLockController.setupButton(act,width);
+    layoutMid.addView(mLockController.getButton());
     layoutRight.addView(button);
 
     layoutBot.addView(layoutLeft);
@@ -66,7 +68,7 @@ abstract class RubikScreenBase extends RubikScreenAbstract
 
   public void setLockState(final RubikActivity act)
     {
-    mController.setLockState(act);
+    mLockController.setState(act);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -74,20 +76,21 @@ abstract class RubikScreenBase extends RubikScreenAbstract
 
   public RubikScreenBase()
     {
-    mController = new MovesAndLockController();
+    mLockController = new LockController();
+    mMovesController= new MovesController();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void addMove(TwistyActivity act, int axis, int row, int angle)
     {
-    mController.addMove(act,axis,row,angle);
+    mMovesController.addMove(act,axis,row,angle);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void reddenLock(final TwistyActivity act)
     {
-    mController.reddenLock(act);
+    mLockController.reddenLock(act);
     }
   }
diff --git a/src/main/java/org/distorted/screens/RubikScreenPlay.java b/src/main/java/org/distorted/screens/RubikScreenPlay.java
index f44fbc08..b536b95b 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPlay.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPlay.java
@@ -262,7 +262,7 @@ public class RubikScreenPlay extends RubikScreenBase
             mObject = obj;
             act.changeObject(list, true);
             adjustLevels(act);
-            mController.clearMoves(act);
+            mMovesController.clearMoves(act);
             }
 
           mObjectPopup.dismiss();
@@ -395,7 +395,7 @@ public class RubikScreenPlay extends RubikScreenBase
       public void onClick(View v)
         {
         act.getPreRender().solveObject();
-        mController.clearMoves(act);
+        mMovesController.clearMoves(act);
         }
       });
     }
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolving.java b/src/main/java/org/distorted/screens/RubikScreenSolving.java
index de39caa3..8395c62e 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolving.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolving.java
@@ -100,7 +100,7 @@ public class RubikScreenSolving extends RubikScreenBase
       @Override
       public void onClick(View v)
         {
-        if( mController.getNumMoves() > MOVES_THRESHHOLD )
+        if( mMovesController.getNumMoves() > MOVES_THRESHHOLD )
           {
           RubikDialogAbandon abaDiag = new RubikDialogAbandon();
           abaDiag.show(act.getSupportFragmentManager(), null);
diff --git a/src/main/java/org/distorted/screens/ScreenList.java b/src/main/java/org/distorted/screens/ScreenList.java
index b53fb2b8..61b2a7c5 100644
--- a/src/main/java/org/distorted/screens/ScreenList.java
+++ b/src/main/java/org/distorted/screens/ScreenList.java
@@ -25,7 +25,7 @@ import android.os.Bundle;
 import com.google.firebase.analytics.FirebaseAnalytics;
 
 import org.distorted.main.RubikActivity;
-import static org.distorted.main.RubikSurfaceView.*;
+import static org.distorted.objectlib.main.ObjectControl.*;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/tutorials/TutorialActivity.java b/src/main/java/org/distorted/tutorials/TutorialActivity.java
index 8636ea75..4d90cbf5 100644
--- a/src/main/java/org/distorted/tutorials/TutorialActivity.java
+++ b/src/main/java/org/distorted/tutorials/TutorialActivity.java
@@ -34,9 +34,6 @@ import com.google.firebase.analytics.FirebaseAnalytics;
 
 import org.distorted.library.main.DistortedLibrary;
 
-import org.distorted.main.RubikSurfaceView;
-import org.distorted.network.RubikScores;
-import org.distorted.objectlib.effects.BaseEffect;
 import org.distorted.objectlib.main.ObjectPreRender;
 import org.distorted.objectlib.main.ObjectType;
 import org.distorted.objectlib.main.TwistyObject;
@@ -45,7 +42,6 @@ import org.distorted.objectlib.helpers.TwistyActivity;
 
 import org.distorted.main.R;
 import org.distorted.dialogs.RubikDialogError;
-import org.distorted.screens.ScreenList;
 
 import static org.distorted.main.RubikRenderer.BRIGHTNESS;
 
@@ -67,7 +63,7 @@ public class TutorialActivity extends TwistyActivity
     private FirebaseAnalytics mFirebaseAnalytics;
     private static int mScreenWidth, mScreenHeight;
     private int mCurrentApiVersion;
-    private TutorialState mState;
+    private TutorialScreen mState;
     private String mURL;
     private int mObjectOrdinal;
     private TutorialWebView mWebView;
@@ -151,7 +147,7 @@ public class TutorialActivity extends TwistyActivity
       final int color = (int)(BRIGHTNESS*255);
       viewR.setBackgroundColor( (0xFF<<24)+(color<<16)+(color<<8)+color);
 
-      if( mState==null ) mState = new TutorialState();
+      if( mState==null ) mState = new TutorialScreen();
 
       mState.createRightPane(this,width);
 
@@ -249,7 +245,7 @@ public class TutorialActivity extends TwistyActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    TutorialState getState()
+    TutorialScreen getState()
       {
       return mState;
       }
@@ -296,7 +292,7 @@ public class TutorialActivity extends TwistyActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public boolean isLocked()
+    public boolean isLocked()
     {
     return retLocked();
     }
diff --git a/src/main/java/org/distorted/tutorials/TutorialObjectStateActioner.java b/src/main/java/org/distorted/tutorials/TutorialObjectStateActioner.java
index 27a0fc0e..69866858 100644
--- a/src/main/java/org/distorted/tutorials/TutorialObjectStateActioner.java
+++ b/src/main/java/org/distorted/tutorials/TutorialObjectStateActioner.java
@@ -21,6 +21,7 @@ package org.distorted.tutorials;
 
 import org.distorted.objectlib.helpers.ObjectStateActioner;
 import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.ObjectType;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -28,5 +29,26 @@ public class TutorialObjectStateActioner implements ObjectStateActioner
 {
    public void onWinEffectFinished(TwistyActivity act, String debug, int scrambleNum) { }
    public void onScrambleEffectFinished(TwistyActivity act) { }
+   public void onBeginRotation(TwistyActivity act) { }
    public void onSolved() { }
+   public int getCurrentColor() { return 0; }
+   public int cubitIsLocked(ObjectType type, int cubit) { return 0; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void onFinishRotation(TwistyActivity act, int axis, int row, int angle)
+     {
+     TutorialActivity tact = (TutorialActivity)act;
+     TutorialScreen state = tact.getState();
+     state.addMove(act,axis, row, angle);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void failedToDrag(TwistyActivity act)
+     {
+     final TutorialActivity tact = (TutorialActivity)act;
+     TutorialScreen state = tact.getState();
+     state.reddenLock(act);
+     }
 }
diff --git a/src/main/java/org/distorted/tutorials/TutorialScreen.java b/src/main/java/org/distorted/tutorials/TutorialScreen.java
new file mode 100644
index 00000000..084424ee
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialScreen.java
@@ -0,0 +1,144 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube 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.                                                           //
+//                                                                                               //
+// Magic Cube 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 Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.tutorials;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import org.distorted.helpers.MovesController;
+import org.distorted.objectlib.main.ObjectPreRender;
+import org.distorted.objectlib.main.ObjectType;
+
+import org.distorted.helpers.LockController;
+import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+import org.distorted.screens.RubikScreenPlay;
+import org.distorted.screens.ScreenList;
+import org.distorted.helpers.TransparentImageButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialScreen
+{
+  private ImageButton mSolveButton, mScrambleButton, mBackButton;
+  private final LockController mLockController;
+  private final MovesController mMovesController;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupSolveButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_solve,R.drawable.ui_medium_cube_solve, R.drawable.ui_big_cube_solve, R.drawable.ui_huge_cube_solve);
+    mSolveButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mSolveButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        act.getPreRender().solveObject();
+        mMovesController.clearMoves(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupScrambleButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_scramble,R.drawable.ui_medium_cube_scramble, R.drawable.ui_big_cube_scramble, R.drawable.ui_huge_cube_scramble);
+    mScrambleButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mScrambleButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
+        int numScrambles = ObjectType.getNumScramble(play.getObject());
+        act.getPreRender().scrambleObject(numScrambles);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_smallback,R.drawable.ui_medium_smallback, R.drawable.ui_big_smallback, R.drawable.ui_huge_smallback);
+    mBackButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectPreRender pre = act.getPreRender();
+        if( pre!=null ) pre.unblockEverything();
+        act.finish();
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createRightPane(final TutorialActivity act, float width)
+    {
+    LinearLayout layout = act.findViewById(R.id.tutorialRightBar);
+    layout.removeAllViews();
+
+    mMovesController.setupButton(act,width);
+    mLockController.setupButton(act,width);
+    setupSolveButton(act,width);
+    setupScrambleButton(act,width);
+    setupBackButton(act,width);
+
+    layout.addView(mSolveButton);
+    layout.addView(mMovesController.getButton());
+    layout.addView(mScrambleButton);
+    layout.addView(mLockController.getButton());
+    layout.addView(mBackButton);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void addMove(TwistyActivity act, int axis, int row, int angle)
+    {
+    mMovesController.addMove(act, axis,row,angle);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public TutorialScreen()
+    {
+    mLockController = new LockController();
+    mMovesController = new MovesController();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reddenLock(final TwistyActivity act)
+    {
+    mLockController.reddenLock(act);
+    }
+}
diff --git a/src/main/java/org/distorted/tutorials/TutorialState.java b/src/main/java/org/distorted/tutorials/TutorialState.java
deleted file mode 100644
index 6653daa4..00000000
--- a/src/main/java/org/distorted/tutorials/TutorialState.java
+++ /dev/null
@@ -1,141 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube 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.                                                           //
-//                                                                                               //
-// Magic Cube 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 Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.tutorials;
-
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-import org.distorted.objectlib.main.ObjectPreRender;
-import org.distorted.objectlib.main.ObjectType;
-
-import org.distorted.helpers.MovesAndLockController;
-import org.distorted.objectlib.helpers.TwistyActivity;
-import org.distorted.main.R;
-import org.distorted.main.RubikActivity;
-import org.distorted.screens.RubikScreenPlay;
-import org.distorted.screens.ScreenList;
-import org.distorted.helpers.TransparentImageButton;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialState
-{
-  private ImageButton mSolveButton, mScrambleButton, mBackButton;
-  private final MovesAndLockController mController;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupSolveButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_solve,R.drawable.ui_medium_cube_solve, R.drawable.ui_big_cube_solve, R.drawable.ui_huge_cube_solve);
-    mSolveButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mSolveButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        act.getPreRender().solveObject();
-        mController.clearMoves(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupScrambleButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_scramble,R.drawable.ui_medium_cube_scramble, R.drawable.ui_big_cube_scramble, R.drawable.ui_huge_cube_scramble);
-    mScrambleButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mScrambleButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
-        int numScrambles = ObjectType.getNumScramble(play.getObject());
-        act.getPreRender().scrambleObject(numScrambles);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_smallback,R.drawable.ui_medium_smallback, R.drawable.ui_big_smallback, R.drawable.ui_huge_smallback);
-    mBackButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectPreRender pre = act.getPreRender();
-        if( pre!=null ) pre.unblockEverything();
-        act.finish();
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createRightPane(final TutorialActivity act, float width)
-    {
-    LinearLayout layout = act.findViewById(R.id.tutorialRightBar);
-    layout.removeAllViews();
-
-    mController.setupPrevButton(act,width);
-    mController.setupLockButton(act,width);
-    setupSolveButton(act,width);
-    setupScrambleButton(act,width);
-    setupBackButton(act,width);
-
-    layout.addView(mSolveButton);
-    layout.addView(mController.getPrevButton());
-    layout.addView(mScrambleButton);
-    layout.addView(mController.getLockButton());
-    layout.addView(mBackButton);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void addMove(TwistyActivity act, int axis, int row, int angle)
-    {
-    mController.addMove(act, axis,row,angle);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public TutorialState()
-    {
-    mController = new MovesAndLockController();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reddenLock(final TwistyActivity act)
-    {
-    mController.reddenLock(act);
-    }
-}
diff --git a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
index 5b3ceef3..8831464a 100644
--- a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
+++ b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
@@ -25,54 +25,28 @@ import android.content.pm.ConfigurationInfo;
 import android.opengl.GLES30;
 import android.opengl.GLSurfaceView;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.view.MotionEvent;
 
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
 import org.distorted.library.main.DistortedScreen;
-import org.distorted.library.type.Static2D;
 import org.distorted.library.type.Static4D;
-import org.distorted.library.main.QuatHelper;
 
+import org.distorted.objectlib.main.ObjectControl;
 import org.distorted.objectlib.helpers.ObjectSurfaceView;
 import org.distorted.objectlib.helpers.TwistyActivity;
 import org.distorted.objectlib.main.Movement;
 import org.distorted.objectlib.main.ObjectPreRender;
-import org.distorted.objectlib.main.TwistyObject;
 
-import static org.distorted.main.RubikSurfaceView.INVALID_POINTER_ID;
-import static org.distorted.main.RubikSurfaceView.NUM_SPEED_PROBES;
-import static org.distorted.main.RubikSurfaceView.ROTATION_SENSITIVITY;
-import static org.distorted.main.RubikSurfaceView.SWIPING_SENSITIVITY;
+import static org.distorted.objectlib.main.ObjectControl.MODE_ROTATE;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceView
 {
-    private final Static4D CAMERA_POINT = new Static4D(0, 0, 0, 0);
+    private ObjectControl mObjectController;
     private TutorialRenderer mRenderer;
-    private ObjectPreRender mPreRender;
-    private Movement mMovement;
-    private boolean mDragging, mBeginningRotation, mContinuingRotation;
-    private int mScreenWidth, mScreenHeight, mScreenMin;
-
-    private float mRotAngle, mInitDistance;
-    private int mPtrID1, mPtrID2;
-    private float mX, mY;
-    private float mStartRotX, mStartRotY;
-    private float mAxisX, mAxisY;
-    private float mRotationFactor;
-    private int mCurrentAxis, mCurrentRow;
-    private float mCurrentAngle, mCurrRotSpeed;
-    private float[] mLastX;
-    private float[] mLastY;
-    private long[] mLastT;
-    private int mFirstIndex, mLastIndex;
-    private int mDensity;
-
-    private static final Static4D mQuat= new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
-    private static final Static4D mTemp= new Static4D(0,0,0,1);
+    private int mScreenWidth, mScreenHeight;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -80,8 +54,7 @@ public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceV
       {
       mScreenWidth = width;
       mScreenHeight= height;
-
-      mScreenMin = Math.min(width, height);
+      mObjectController.setScreenSize(width,height);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -93,438 +66,16 @@ public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceV
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    ObjectPreRender getPreRender()
-      {
-      return mPreRender;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// cast the 3D axis we are currently rotating along (which is already casted to the surface of the
-// currently touched face AND converted into a 4D vector - fourth 0) to a 2D in-screen-surface axis
-
-    private void computeCurrentAxis(Static4D axis)
-      {
-      Static4D result = QuatHelper.rotateVectorByQuat(axis, mQuat);
-
-      mAxisX =result.get0();
-      mAxisY =result.get1();
-
-      float len = (float)Math.sqrt(mAxisX*mAxisX + mAxisY*mAxisY);
-      mAxisX /= len;
-      mAxisY /= len;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void addSpeedProbe(float x, float y)
-      {
-      long currTime = System.currentTimeMillis();
-      boolean theSame = mLastIndex==mFirstIndex;
-
-      mLastIndex++;
-      if( mLastIndex>=NUM_SPEED_PROBES ) mLastIndex=0;
-
-      mLastT[mLastIndex] = currTime;
-      mLastX[mLastIndex] = x;
-      mLastY[mLastIndex] = y;
-
-      if( mLastIndex==mFirstIndex)
-        {
-        mFirstIndex++;
-        if( mFirstIndex>=NUM_SPEED_PROBES ) mFirstIndex=0;
-        }
-
-      if( theSame )
-        {
-        mLastT[mFirstIndex] = currTime;
-        mLastX[mFirstIndex] = x;
-        mLastY[mFirstIndex] = y;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void computeCurrentSpeedInInchesPerSecond()
-      {
-      long firstTime = mLastT[mFirstIndex];
-      long lastTime  = mLastT[mLastIndex];
-      float fX = mLastX[mFirstIndex];
-      float fY = mLastY[mFirstIndex];
-      float lX = mLastX[mLastIndex];
-      float lY = mLastY[mLastIndex];
-
-      long timeDiff = lastTime-firstTime;
-
-      mLastIndex = 0;
-      mFirstIndex= 0;
-
-      mCurrRotSpeed = timeDiff>0 ? 1000*retFingerDragDistanceInInches(fX,fY,lX,lY)/timeDiff : 0;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float retFingerDragDistanceInInches(float xFrom, float yFrom, float xTo, float yTo)
-      {
-      float xDist = mScreenWidth*(xFrom-xTo);
-      float yDist = mScreenHeight*(yFrom-yTo);
-      float distInPixels = (float)Math.sqrt(xDist*xDist + yDist*yDist);
-
-      return distInPixels/mDensity;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void setUpDragOrRotate(float x, float y)
-      {
-      TwistyObject object = mPreRender.getObject();
-      CAMERA_POINT.set2( object==null ? 1.21f : object.getCameraDist() );
-
-      Static4D touchPoint = new Static4D(x, y, 0, 0);
-      Static4D rotatedTouchPoint= QuatHelper.rotateVectorByInvertedQuat(touchPoint, mQuat);
-      Static4D rotatedCamera= QuatHelper.rotateVectorByInvertedQuat(CAMERA_POINT, mQuat);
-
-      if( mMovement!=null && mMovement.faceTouched(rotatedTouchPoint,rotatedCamera,object.getObjectRatio()) )
-        {
-        mDragging           = false;
-        mContinuingRotation = false;
-        mBeginningRotation= !mPreRender.isTouchBlocked();
-        }
-      else
-        {
-        final TutorialActivity act = (TutorialActivity)getContext();
-        boolean locked      = act.isLocked();
-        mDragging           = !locked;
-        mContinuingRotation = false;
-        mBeginningRotation  = false;
-
-        if( !mDragging )
-          {
-          TutorialState state = act.getState();
-          state.reddenLock(act);
-          }
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void drag(MotionEvent event, float x, float y)
-      {
-      if( mPtrID1!=INVALID_POINTER_ID && mPtrID2!=INVALID_POINTER_ID)
-        {
-        int pointer = event.findPointerIndex(mPtrID2);
-        float pX,pY;
-
-        try
-          {
-          pX = event.getX(pointer);
-          pY = event.getY(pointer);
-          }
-        catch(IllegalArgumentException ex)
-          {
-          mPtrID1=INVALID_POINTER_ID;
-          mPtrID2=INVALID_POINTER_ID;
-
-          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-          crashlytics.setCustomKey("DragError", "pointer="+pointer );
-          crashlytics.recordException(ex);
-
-          return;
-          }
-
-        float x2 = (pX - mScreenWidth*0.5f)/mScreenMin;
-        float y2 = (mScreenHeight*0.5f -pY)/mScreenMin;
-
-        float angleNow = getAngle(x,y,x2,y2);
-        float angleDiff = angleNow-mRotAngle;
-        float sinA =-(float)Math.sin(angleDiff);
-        float cosA = (float)Math.cos(angleDiff);
-
-        Static4D dragQuat = QuatHelper.quatMultiply(new Static4D(0,0,sinA,cosA), mQuat);
-        mTemp.set(dragQuat);
-
-        mRotAngle = angleNow;
-
-        float distNow  = (float)Math.sqrt( (x-x2)*(x-x2) + (y-y2)*(y-y2) );
-        float distQuot = mInitDistance<0 ? 1.0f : distNow/ mInitDistance;
-        mInitDistance = distNow;
-
-        TwistyObject object = mPreRender.getObject();
-        if( object!=null ) object.setObjectRatio(distQuot);
-        }
-      else
-        {
-        Static4D dragQuat = QuatHelper.quatMultiply(QuatHelper.quatFromDrag(mX-x,y-mY), mQuat);
-        mTemp.set(dragQuat);
-        }
-
-      mPreRender.setQuatOnNextRender();
-      mX = x;
-      mY = y;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void finishRotation()
-      {
-      computeCurrentSpeedInInchesPerSecond();
-      int angle = mPreRender.getObject().computeNearestAngle(mCurrentAxis,mCurrentAngle, mCurrRotSpeed);
-      mPreRender.finishRotation(angle);
-
-      if( angle!=0 )
-        {
-        final TutorialActivity act = (TutorialActivity)getContext();
-        TutorialState state = act.getState();
-        state.addMove(act,mCurrentAxis, mCurrentRow, angle);
-        }
-
-      mContinuingRotation = false;
-      mBeginningRotation  = false;
-      mDragging           = true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void continueRotation(float x, float y)
-      {
-      float dx = x-mStartRotX;
-      float dy = y-mStartRotY;
-      float alpha = dx*mAxisX + dy*mAxisY;
-      float x2 = dx - alpha*mAxisX;
-      float y2 = dy - alpha*mAxisY;
-
-      float len = (float)Math.sqrt(x2*x2 + y2*y2);
-
-      // we have the length of 1D vector 'angle', now the direction:
-      float tmp = mAxisY==0 ? -mAxisX*y2 : mAxisY*x2;
-
-      float angle = (tmp>0 ? 1:-1)*len*mRotationFactor;
-      mCurrentAngle = SWIPING_SENSITIVITY*angle;
-      mPreRender.getObject().continueRotation(mCurrentAngle);
-
-      addSpeedProbe(x2,y2);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void beginRotation(float x, float y)
-      {
-      mStartRotX = x;
-      mStartRotY = y;
-
-      TwistyObject object = mPreRender.getObject();
-      int numLayers = object.getNumLayers();
-
-      Static4D touchPoint2 = new Static4D(x, y, 0, 0);
-      Static4D rotatedTouchPoint2= QuatHelper.rotateVectorByInvertedQuat(touchPoint2, mQuat);
-      Static2D res = mMovement.newRotation(rotatedTouchPoint2,object.getObjectRatio());
-
-      mCurrentAxis = (int)res.get0();
-      mCurrentRow  = (int)res.get1();
-
-      computeCurrentAxis( mMovement.getCastedRotAxis(mCurrentAxis) );
-      mRotationFactor = mMovement.returnRotationFactor(numLayers,mCurrentRow);
-
-      object.beginNewRotation( mCurrentAxis, mCurrentRow );
-
-      addSpeedProbe(x,y);
-
-      mBeginningRotation = false;
-      mContinuingRotation= true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float getAngle(float x1, float y1, float x2, float y2)
+    TutorialRenderer getRenderer()
       {
-      return (float) Math.atan2(y1-y2, x1-x2);
+      return mRenderer;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void actionMove(MotionEvent event)
-      {
-      int pointer = event.findPointerIndex(mPtrID1 != INVALID_POINTER_ID ? mPtrID1:mPtrID2);
-
-      if( pointer<0 ) return;
-
-      float pX = event.getX(pointer);
-      float pY = event.getY(pointer);
-
-      float x = (pX - mScreenWidth*0.5f)/mScreenMin;
-      float y = (mScreenHeight*0.5f -pY)/mScreenMin;
-
-      if( mBeginningRotation )
-        {
-        if( retFingerDragDistanceInInches(mX,mY,x,y) > ROTATION_SENSITIVITY )
-          {
-          beginRotation(x,y);
-          }
-        }
-      else if( mContinuingRotation )
-        {
-        continueRotation(x,y);
-        }
-      else if( mDragging )
-        {
-        drag(event,x,y);
-        }
-      else
-        {
-        setUpDragOrRotate(x,y);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionDown(MotionEvent event)
-      {
-      mPtrID1 = event.getPointerId(0);
-
-      float x = event.getX();
-      float y = event.getY();
-
-      mX = (x - mScreenWidth*0.5f)/mScreenMin;
-      mY = (mScreenHeight*0.5f -y)/mScreenMin;
-
-      setUpDragOrRotate(mX,mY);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionUp(MotionEvent event)
-      {
-      mPtrID1 = INVALID_POINTER_ID;
-      mPtrID2 = INVALID_POINTER_ID;
-
-      if( mContinuingRotation )
-        {
-        finishRotation();
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionDown2(MotionEvent event)
-      {
-      int index = event.getActionIndex();
-
-      if( mPtrID1==INVALID_POINTER_ID )
-        {
-        mPtrID1 = event.getPointerId(index);
-        float x = event.getX();
-        float y = event.getY();
-
-        if( mPtrID2 != INVALID_POINTER_ID )
-          {
-          int pointer = event.findPointerIndex(mPtrID2);
-
-          try
-            {
-            float x2 = event.getX(pointer);
-            float y2 = event.getY(pointer);
-
-            mRotAngle = getAngle(x,-y,x2,-y2);
-            mInitDistance = -1;
-            }
-          catch(IllegalArgumentException ex)
-            {
-            mPtrID1=INVALID_POINTER_ID;
-            mPtrID2=INVALID_POINTER_ID;
-
-            FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-            crashlytics.setCustomKey("DragError", "pointer="+pointer );
-            crashlytics.recordException(ex);
-
-            return;
-            }
-          }
-
-        mX = (x - mScreenWidth*0.5f)/mScreenMin;
-        mY = (mScreenHeight*0.5f -y)/mScreenMin;
-        }
-      else if( mPtrID2==INVALID_POINTER_ID )
-        {
-        mPtrID2 = event.getPointerId(index);
-
-        float x = event.getX();
-        float y = event.getY();
-
-        if( mPtrID2 != INVALID_POINTER_ID )
-          {
-          int pointer = event.findPointerIndex(mPtrID2);
-
-          try
-            {
-            float x2 = event.getX(pointer);
-            float y2 = event.getY(pointer);
-
-            mRotAngle = getAngle(x,-y,x2,-y2);
-            mInitDistance = -1;
-            }
-          catch(IllegalArgumentException ex)
-            {
-            mPtrID1=INVALID_POINTER_ID;
-            mPtrID2=INVALID_POINTER_ID;
-
-            FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-            crashlytics.setCustomKey("DragError", "pointer="+pointer );
-            crashlytics.recordException(ex);
-
-            return;
-            }
-          }
-
-        if( mBeginningRotation || mContinuingRotation )
-          {
-          mX = (x - mScreenWidth*0.5f)/mScreenMin;
-          mY = (mScreenHeight*0.5f -y)/mScreenMin;
-          }
-        }
-
-      if( mBeginningRotation )
-        {
-        mContinuingRotation = false;
-        mBeginningRotation  = false;
-        mDragging           = true;
-        }
-      else if( mContinuingRotation )
-        {
-        finishRotation();
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionUp2(MotionEvent event)
-      {
-      int index = event.getActionIndex();
-
-      if( index==event.findPointerIndex(mPtrID1) )
-        {
-        mPtrID1 = INVALID_POINTER_ID;
-        int pointer = event.findPointerIndex(mPtrID2);
-
-        if( pointer>=0 )
-          {
-          float x1 = event.getX(pointer);
-          float y1 = event.getY(pointer);
-
-          mX = (x1 - mScreenWidth*0.5f)/mScreenMin;
-          mY = (mScreenHeight*0.5f -y1)/mScreenMin;
-          }
-        }
-      else if( index==event.findPointerIndex(mPtrID2) )
-        {
-        mPtrID2 = INVALID_POINTER_ID;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void initialize()
+    ObjectPreRender getPreRender()
       {
-      mPtrID1 = INVALID_POINTER_ID;
-      mPtrID2 = INVALID_POINTER_ID;
+      return mObjectController.getPreRender();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -537,22 +88,9 @@ public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceV
 
       if(!isInEditMode())
         {
-        mCurrRotSpeed= 0.0f;
-
-        mLastX = new float[NUM_SPEED_PROBES];
-        mLastY = new float[NUM_SPEED_PROBES];
-        mLastT = new long[NUM_SPEED_PROBES];
-        mFirstIndex =0;
-        mLastIndex  =0;
-
-        mRenderer  = new TutorialRenderer(this);
-        mPreRender = new ObjectPreRender(this,new TutorialObjectStateActioner());
-
         TutorialActivity act = (TutorialActivity)context;
-        DisplayMetrics dm = new DisplayMetrics();
-        act.getWindowManager().getDefaultDisplay().getMetrics(dm);
-
-        mDensity = dm.densityDpi;
+        mRenderer = new TutorialRenderer(this);
+        mObjectController = new ObjectControl(act,this,new TutorialObjectStateActioner());
 
         final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
 
@@ -574,9 +112,9 @@ public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceV
 
           FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
           crashlytics.setCustomKey("GLSL Version"  , shading );
-          crashlytics.setCustomKey("GLversion"     , version );
+          crashlytics.setCustomKey("GL version"    , version );
           crashlytics.setCustomKey("GL Vendor "    , vendor  );
-          crashlytics.setCustomKey("GLSLrenderer"  , renderer);
+          crashlytics.setCustomKey("GLSL renderer" , renderer);
           crashlytics.recordException(ex);
           }
         }
@@ -584,23 +122,23 @@ public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceV
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void setQuat()
+    public void setMovement(Movement movement)
       {
-      mQuat.set(mTemp);
+      mObjectController.setMovement(movement);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public Static4D getQuat()
+    public void setQuat()
       {
-      return mQuat;
+      mObjectController.setQuat();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void setMovement(Movement movement)
+    public Static4D getQuat()
       {
-      mMovement = movement;
+      return mObjectController.getQuat();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -617,23 +155,19 @@ public class TutorialSurfaceView extends GLSurfaceView implements ObjectSurfaceV
       return mRenderer.getScreen();
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void initialize()
+      {
+      mObjectController.initialize();
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     @Override
     public boolean onTouchEvent(MotionEvent event)
       {
-      int action = event.getActionMasked();
-
-      switch(action)
-         {
-         case MotionEvent.ACTION_DOWN        : actionDown(event) ; break;
-         case MotionEvent.ACTION_MOVE        : actionMove(event) ; break;
-         case MotionEvent.ACTION_UP          : actionUp(event)   ; break;
-         case MotionEvent.ACTION_POINTER_DOWN: actionDown2(event); break;
-         case MotionEvent.ACTION_POINTER_UP  : actionUp2(event)  ; break;
-         }
-
-      return true;
+      return mObjectController.onTouchEvent(event,MODE_ROTATE);
       }
 }
 
