commit 4a014840b618290b6f1a6c672dd0bedb0f62119f
Author: leszek <leszek@koltunski.pl>
Date:   Sun Dec 3 23:12:07 2023 +0100

    progress with Ghost rotations.

diff --git a/src/main/java/org/distorted/objectlib/main/ObjectControl.java b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
index 144ca91f..09dc0b29 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectControl.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
@@ -332,7 +332,7 @@ public class ObjectControl
       mRotationFactor = mTouchControl.returnRotationFactor(mCurrentAxis,mCurrentRow);
       if( mRotAxisType != ROT_AXIS_NOT_NORMAL ) mNormalRotAngle = computeNormalRotAngle(mNormalTouchPoint[0]-x,mNormalTouchPoint[1]-y);
 
-      if( object.beginNewRotation( mCurrentAxis, mCurrentRow ) )
+      if( object.beginRotation( mCurrentAxis, mCurrentRow ) )
         {
         mInterface.onBeginRotation();
         addSpeedProbe(0);
@@ -823,13 +823,6 @@ public class ObjectControl
       return mPreRender.getObject();
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public OperatingSystemInterface getOS()
-      {
-      return mOS;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void savePreferences()
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
index cd3839a9..2fb44a80 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
@@ -42,7 +42,7 @@ public class ObjectPreRender implements EffectListener
   private float mMoveX, mMoveY;
   private float mScale;
 
-  private boolean mFinishRotation, mRemoveRotation, mRemovePatternRotation, mAddRotation,
+  private boolean mFinishRotation, mRemoveManualRotation, mRemoveProgramaticRotation, mAddRotation,
                   mSetQuat, mChangeObject, mSolveObject, mScrambleObject, mFastScrambleObject,
                   mPresentObject,mInitializeObject, mSetTextureMap, mResetAllTextureMaps, mSolve,
                   mApplyScrambles, mResetTextureEffect;
@@ -54,7 +54,7 @@ public class ObjectPreRender implements EffectListener
   private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
   private long mAddRotationDuration;
   private int mPresentDuration, mRestickerDuration;
-  private long mAddRotationID, mRemoveRotationID;
+  private long mAddRotationID, mRemoveProgramaticRotationID;
   private int mCubit, mFace, mNewColor;
   private int mFinishAxis, mFinishRow;
   private float mFinishAngle, mAvgSpeed;
@@ -72,8 +72,8 @@ public class ObjectPreRender implements EffectListener
     mController = controller;
 
     mFinishRotation       = false;
-    mRemoveRotation       = false;
-    mRemovePatternRotation= false;
+    mRemoveManualRotation = false;
+    mRemoveProgramaticRotation = false;
     mAddRotation          = false;
     mSetQuat              = false;
     mChangeObject         = false;
@@ -175,10 +175,10 @@ public class ObjectPreRender implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void removeRotationNow()
+  private void removeManualRotationNow()
     {
-    mRemoveRotation=false;
-    float angle = mNewObject.removeRotationNow();
+    mRemoveManualRotation =false;
+    float angle = mNewObject.removeRotation();
 
     boolean solved = mNewObject.isSolved();
 
@@ -209,25 +209,25 @@ public class ObjectPreRender implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void removeRotation()
+  private void removeManualRotation()
     {
-    mRemoveRotation = true;
+    mRemoveManualRotation = true;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void removePatternRotation()
+  private void removeProgramaticRotation()
     {
-    mRemovePatternRotation = true;
+    mRemoveProgramaticRotation = true;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void removePatternRotationNow()
+  private void removeProgramaticRotationNow()
     {
-    mRemovePatternRotation=false;
-    mNewObject.removeRotationNow();
-    mAddActionListener.onActionFinished(mRemoveRotationID);
+    mRemoveProgramaticRotation =false;
+    mNewObject.removeRotation();
+    mAddActionListener.onActionFinished(mRemoveProgramaticRotationID);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -238,8 +238,8 @@ public class ObjectPreRender implements EffectListener
 
     if( mNewObject.getNumAxis() > mAddRotationAxis )
       {
-      mAddRotationID = mNewObject.addNewRotation( this, mAddRotationAxis, mAddRotationRowBitmap,
-                                                        mAddRotationAngle, mAddRotationDuration);
+      mAddRotationID = mNewObject.addRotation( this, mAddRotationAxis, mAddRotationRowBitmap,
+                                                     mAddRotationAngle, mAddRotationDuration);
 
       // failed to add effect (previous rotation hasn't been removed yet)
       if( mAddRotationID==0 )
@@ -276,7 +276,7 @@ public class ObjectPreRender implements EffectListener
     {
     mFinishRotation = false;
     blockEverything(BlockController.PLACE_0);
-    mRotationFinishedID = mNewObject.finishRotationNow(this,mFinishAxis,mFinishRow,mFinishAngle,mAvgSpeed);
+    mRotationFinishedID = mNewObject.finishRotation(this,mFinishAxis,mFinishRow,mFinishAngle,mAvgSpeed);
 
     if( mRotationFinishedID==0 ) // failed to add effect - should never happen
       {
@@ -608,22 +608,22 @@ public class ObjectPreRender implements EffectListener
 
   public void preRender()
     {
-    if( mSolve                 ) solveNow();
-    if( mSetQuat               ) setQuatNow();
-    if( mFinishRotation        ) finishRotationNow();
-    if( mRemoveRotation        ) removeRotationNow();
-    if( mRemovePatternRotation ) removePatternRotationNow();
-    if( mChangeObject          ) changeObjectNow();
-    if( mSolveObject           ) solveObjectNow();
-    if( mScrambleObject        ) scrambleObjectNow();
-    if( mFastScrambleObject    ) fastScrambleObjectNow();
-    if( mPresentObject         ) presentObjectNow();
-    if( mAddRotation           ) addRotationNow();
-    if( mInitializeObject      ) initializeObjectNow();
-    if( mApplyScrambles        ) applyScramblesNow();
-    if( mResetAllTextureMaps   ) resetAllTextureMapsNow();
-    if( mSetTextureMap         ) setTextureMapNow();
-    if( mResetTextureEffect    ) resetTextureNow();
+    if( mSolve                    ) solveNow();
+    if( mSetQuat                  ) setQuatNow();
+    if( mFinishRotation           ) finishRotationNow();
+    if( mRemoveManualRotation     ) removeManualRotationNow();
+    if( mRemoveProgramaticRotation) removeProgramaticRotationNow();
+    if( mChangeObject             ) changeObjectNow();
+    if( mSolveObject              ) solveObjectNow();
+    if( mScrambleObject           ) scrambleObjectNow();
+    if( mFastScrambleObject       ) fastScrambleObjectNow();
+    if( mPresentObject            ) presentObjectNow();
+    if( mAddRotation              ) addRotationNow();
+    if( mInitializeObject         ) initializeObjectNow();
+    if( mApplyScrambles           ) applyScramblesNow();
+    if( mResetAllTextureMaps      ) resetAllTextureMapsNow();
+    if( mSetTextureMap            ) setTextureMapNow();
+    if( mResetTextureEffect       ) resetTextureNow();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -631,9 +631,8 @@ public class ObjectPreRender implements EffectListener
   public void addRotation(MovesFinished listener, int axis, int rowBitmap, int bareAngle, int millPreDegree)
     {
     int[][] basicAngles = mNewObject==null ? null : mNewObject.getBasicAngles();
-    int length = basicAngles==null ? 0 : basicAngles.length;
 
-    if( axis<length )
+    if( basicAngles!=null && axis<basicAngles.length )
       {
       int row = computeRowFromBitmap(rowBitmap);
 
@@ -771,13 +770,13 @@ public class ObjectPreRender implements EffectListener
     if( effectID == mRotationFinishedID )
       {
       mRotationFinishedID = 0;
-      removeRotation();
+      removeManualRotation();
       }
     else if( effectID == mAddRotationID )
       {
       mAddRotationID = 0;
-      mRemoveRotationID = effectID;
-      removePatternRotation();
+      mRemoveProgramaticRotationID = effectID;
+      removeProgramaticRotation();
       }
     else
       {
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java b/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java
index bf69b785..413b74a0 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java
@@ -10,6 +10,7 @@
 package org.distorted.objectlib.main;
 
 import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.message.EffectListener;
 import org.distorted.library.type.Dynamic1D;
 import org.distorted.library.type.Static1D;
@@ -30,20 +31,37 @@ public class TwistyLayerRotations
   private int mCurrentRotAxis;
 
   private final int mMaxNumLayers;
-  private final int mGhostAngle;
   private final Static3D mRotationAxis;
   private final VertexEffectRotate mRotateEffect;
   private final Dynamic1D mRotationAngle;
   private final Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
   private final TwistyObject mParent;
 
+  // ghost angle
+  private final float mGhostAngle;
+  private final int mNumLayer0;
+  private final boolean[] mGhostIsUp;
+  private int mUpLeft, mUpRight;
+  private final int mNumLeft, mNumRight;
+  private VertexEffectRotate mGhostEffect1;
+  private Static1D mGhostAngleN;
+  private int mGhostN;
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyLayerRotations(TwistyObject parent, int maxLayers, int ghostAngle)
+  TwistyLayerRotations(TwistyObject parent, Static3D ax, int[] numLayers, int ghostAngle, DistortedEffects effect)
     {
     mParent = parent;
+    mGhostAngle = (float)(Math.PI*ghostAngle/180);
+
+    int maxLayers = -1;
+    for(int numLayer : numLayers) if(maxLayers<numLayer) maxLayers = numLayer;
     mMaxNumLayers = maxLayers;
-    mGhostAngle = ghostAngle;
+    mNumLayer0 = numLayers[0];
+    mGhostIsUp = (mGhostAngle==0) ? null : new boolean[mNumLayer0];
+    mUpLeft = mUpRight = 0;
+    mNumLeft = mNumLayer0/2;
+    mNumRight= mNumLayer0-mNumLeft;
 
     mRotationState = STATE_NOTHING;
 
@@ -54,41 +72,214 @@ public class TwistyLayerRotations
     mRotationAngle= new Dynamic1D();
     mRotationAxis = new Static3D(1,0,0);
     mRotateEffect = new VertexEffectRotate(mRotationAngle, mRotationAxis, CENTER);
+
+    effect.apply(mRotateEffect);
+
+    if( mGhostAngle!=0 )
+      {
+      mGhostN = 0;
+      Static1D ghostAngle1= new Static1D( (float)(Math.PI*mGhostAngle/180) );
+      mGhostAngleN= new Static1D(0);
+      VertexEffectRotate ghostEffectN = new VertexEffectRotate(mGhostAngleN, ax, CENTER);
+      mGhostEffect1 = new VertexEffectRotate( ghostAngle1, ax, CENTER);
+      effect.apply(ghostEffectN);
+      effect.apply(mGhostEffect1);
+      }
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// GHOST
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private int computeNearestAngle(int basicAngle, float angle, float speed)
+  private boolean angleIsGhost(int angle, int basicAngle)
     {
-    int nearestAngle = 360/basicAngle;
-    int tmp = (int)((angle+nearestAngle/2)/nearestAngle);
-    if( angle< -(nearestAngle*0.5) ) tmp-=1;
+    return angle%(360/basicAngle)!=0;
+    }
 
-    if( tmp!=0 ) return nearestAngle*tmp;
+///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    return speed> 1.2f ? nearestAngle*(angle>0 ? 1:-1) : 0;
+  private int computeGhostAngle(int row, int basicAngle, float angle, float speed)
+    {
+    boolean left = row<mNumLayer0/2;
+
+    if( left )
+      {
+      if( angle<0 )  // if we are looking at the 0th axis (x axis) from the front, this is 'up' rot
+        {
+        if( mUpRight==mNumRight && !mGhostIsUp[row] )
+          {
+          int a = computeGhostAngle(basicAngle,angle,speed);
+          if( angleIsGhost(a,basicAngle) )
+            {
+            changeRow(row,true);
+            if( mUpLeft==0 ) enableGhostAxis(false);
+            mUpLeft++;
+
+            if( mUpLeft==mNumLeft )
+              {
+              for( int i=0; i<mNumLayer0; i++ ) changeRow(i,false);
+              mUpLeft = 0;
+              mUpRight= 0;
+              changeTotalGhostAngle(1);
+              }
+            }
+          return a;
+          }
+        else return computeNearestAngle(basicAngle,angle,speed);
+        }
+      else
+        {
+        if( mGhostIsUp[row] )
+          {
+          int a = computeGhostAngle(basicAngle,angle,speed);
+          if( angleIsGhost(a,basicAngle) )
+            {
+            changeRow(row,false);
+            if( mUpLeft==1 ) enableGhostAxis(true);
+            mUpLeft--;
+            }
+          return a;
+          }
+        else if( mUpLeft==0 && mUpRight==0 )
+          {
+          int a = computeGhostAngle(basicAngle,angle,speed);
+          if( angleIsGhost(a,basicAngle) )
+            {
+            mUpLeft = mNumLeft-1;
+            mUpRight= mNumRight;
+            for( int i=0; i<mNumLayer0; i++ )
+              if( i!=row) changeRow(i,true);
+            if( mNumLeft==1 ) enableGhostAxis(true);
+            changeTotalGhostAngle(-1);
+            }
+          return a;
+          }
+        else return computeNearestAngle(basicAngle,angle,speed);
+        }
+      }
+    else
+      {
+      if( angle<0 )
+        {
+        if( !mGhostIsUp[row] )
+          {
+          int a = computeGhostAngle(basicAngle,angle,speed);
+          if( angleIsGhost(a,basicAngle) )
+            {
+            changeRow(row,true);
+            mUpRight++;
+            if( mUpRight==mNumRight ) enableGhostAxis(true);
+            }
+          return a;
+          }
+        else return computeNearestAngle(basicAngle,angle,speed);
+        }
+      else
+        {
+        if( mGhostIsUp[row] )
+          {
+          int a = computeGhostAngle(basicAngle,angle,speed);
+          if( angleIsGhost(a,basicAngle) )
+            {
+            changeRow(row,false);
+            mUpRight--;
+            if( mNumRight==1 ) enableGhostAxis(false);
+            }
+          return a;
+          }
+        else return computeNearestAngle(basicAngle,angle,speed);
+        }
+      }
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private float getAngle()
+  private int computeRowBitmap(boolean[] up)
     {
-    int pointNum = mRotationAngle.getNumPoints();
-    return pointNum>=1 ? mRotationAngle.getPoint(pointNum-1).get0() : 0;
+    int ret = 0;
+    int tmp = 1;
+
+    for(int l=0; l<mNumLayer0; l++)
+      {
+      if( up[l] ) ret += tmp;
+      tmp *= 2;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void enableGhostAxis(boolean enable)
+    {
+    mParent.enableGhostAxis(enable);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void changeTotalGhostAngle(int diff)
+    {
+    mGhostN += diff;
+    mGhostN %= 360;
+    mGhostAngleN.set0(mGhostN*mGhostAngle);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void changeRow(int row, boolean up)
+    {
+    mGhostIsUp[row] = up;
+    int rowBitmap = computeRowBitmap(mGhostIsUp);
+    mGhostEffect1.setMeshAssociation( rowBitmap , -1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// TODO
+
+  private int computeGhostAngle(int basicAngle, float angle, float speed)
+    {
+    int basicDeg = 360/basicAngle;
+    int numRot = (int)(angle/basicDeg + 0.5f);
+    if( angle< -(basicDeg*0.5) ) numRot-=1;
+    if( numRot==0 && speed>1.1f ) numRot = (angle>0 ? 1:-1);
+    return basicDeg*numRot;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// END GHOST
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeNearestAngle(int basicAngle, float angle, float speed)
+    {
+    int basicDeg = 360/basicAngle;
+    int numRot = (int)(angle/basicDeg + 0.5f);
+    if( angle< -(basicDeg*0.5) ) numRot-=1;
+    if( numRot==0 && speed>1.1f ) numRot = (angle>0 ? 1:-1);
+    return basicDeg*numRot;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeNearestAngle(int axisIndex, int row, int basicAngle, float angle, float speed)
+    {
+    if( mGhostAngle==0 || axisIndex!=0 ) return computeNearestAngle(basicAngle,angle,speed);
+    else                                 return computeGhostAngle(row,basicAngle,angle,speed);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  VertexEffectRotate getEffect()
+  private float getAngle()
     {
-    return mRotateEffect;
+    int pointNum = mRotationAngle.getNumPoints();
+    return pointNum>=1 ? mRotationAngle.getPoint(pointNum-1).get0() : 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// the rotation has fully finished (everything including the last mini 'come back'). Delete it.
 
-  synchronized float removeRotationNow()
+  synchronized float removeRotation()
     {
     float angle = getAngle();
 
@@ -101,10 +292,11 @@ public class TwistyLayerRotations
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// we have just lifted the finger
 
-  long finishRotationNow(EffectListener listener, int basicAngle, float finishAngle, float avgSpeed)
+  long finishRotation(EffectListener listener, int axisIndex, int row, int basicAngle, float finishAngle, float avgSpeed)
     {
-    int nearestAngleInDegrees = computeNearestAngle(basicAngle,finishAngle,avgSpeed);
+    int nearestAngleInDegrees = computeNearestAngle(axisIndex,row,basicAngle,finishAngle,avgSpeed);
 
     mRotationState = STATE_FINISH;
     float angle = getAngle();
@@ -124,6 +316,7 @@ public class TwistyLayerRotations
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotation has begun and we have just moved the finger a bit
 
   void continueRotation(float angleInDegrees)
     {
@@ -131,17 +324,18 @@ public class TwistyLayerRotations
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// this is 'programmatic' rotation - i.e. for example the one induced by the 'backMove' button.
 
-  synchronized long addNewRotation(EffectListener listener, Static3D axis, int axisIndex, int rowBitmap, int angle, long durationMillis )
+  synchronized long addRotation(EffectListener listener, Static3D axis, int axisIndex, int rowBitmap, int angle, long durationMillis )
     {
     int mult = 1;
 
     if( mRotationState==STATE_ROTATE ) return 0;
-    if( mRotationState==STATE_FINISH ) { removeRotationNow(); mult = -1; }
+    if( mRotationState==STATE_FINISH ) { removeRotation(); mult = -1; }
 
     mRotationState = STATE_ROTATE;
-    mCurrentRotAxis = axisIndex;
-    mRotRowBitmap= mParent.computeBandagedBitmap(rowBitmap,axisIndex);
+    mCurrentRotAxis= axisIndex;
+    mRotRowBitmap  = mParent.computeBandagedBitmap(rowBitmap,axisIndex);
     mRotationAngleStatic.set0(0.0f);
     mRotationAxis.set(axis);
     mRotationAngle.setDuration(durationMillis);
@@ -155,15 +349,17 @@ public class TwistyLayerRotations
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+// i.e. we just touched the screen, moved it a little bit and the TouchControl has figured out
+// along which axis we are going to rotate.
 
-  synchronized boolean beginNewRotation(Static3D axis, int axisIndex, int row )
+  synchronized boolean beginRotation(Static3D axis, int axisIndex, int row )
     {
     if( mRotationState==STATE_ROTATE ) return false;
-    if( mRotationState==STATE_FINISH ) removeRotationNow();
+    if( mRotationState==STATE_FINISH ) removeRotation();
 
     mRotationState = STATE_ROTATE;
-    mCurrentRotAxis = axisIndex;
-    mRotRowBitmap= mParent.computeBandagedBitmap( (1<<row),axisIndex );
+    mCurrentRotAxis= axisIndex;
+    mRotRowBitmap  = mParent.computeBandagedBitmap( (1<<row),axisIndex );
     mRotationAngleStatic.set0(0.0f);
     mRotationAxis.set(axis);
     mRotationAngle.add(mRotationAngleStatic);
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObject.java b/src/main/java/org/distorted/objectlib/main/TwistyObject.java
index e7e56875..1111c872 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyObject.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObject.java
@@ -229,8 +229,6 @@ public abstract class TwistyObject
     mNumFaceColors = getNumFaceColors();
     mBelongs = new boolean[mNumCubits];
 
-    mRotation = new TwistyLayerRotations(this,mMaxNumLayers,getGhostAngle());
-
     int scramblingType = getScrambleType();
     int[][] edges = getScrambleEdges();
     int[][] algorithms = getScrambleAlgorithms();
@@ -258,7 +256,8 @@ public abstract class TwistyObject
     mEffects = new DistortedEffects();
     createQuaternionEffects();
 
-    VertexEffectRotate rotateEffect = mRotation.getEffect();
+    mRotation = new TwistyLayerRotations(this,mAxis[0],mNumLayers,getGhostAngle(),mEffects);
+
     MatrixEffectScale scaleEffect = new MatrixEffectScale(mObjectScale);
     MatrixEffectQuaternion quatEffect = new MatrixEffectQuaternion(mQuat, CENTER);
     MatrixEffectMove moveEffect = new MatrixEffectMove(move);
@@ -273,7 +272,6 @@ public abstract class TwistyObject
     mSolved = new TwistyObjectSolved(this,mOrigPos,index);
     mSolved.setupSolvedQuats(getSolvedQuats());
 
-    mEffects.apply(rotateEffect);
     mEffects.apply(quatEffect);
     mEffects.apply(scaleEffect);
     mEffects.apply(moveEffect);
@@ -1103,17 +1101,17 @@ public abstract class TwistyObject
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  synchronized float removeRotationNow()
+  synchronized float removeRotation()
     {
-    return mRotation.removeRotationNow();
+    return mRotation.removeRotation();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  long finishRotationNow(EffectListener listener, int finishAx, int finishRow, float finishAngle, float avgSpeed)
+  long finishRotation(EffectListener listener, int finishAx, int finishRow, float finishAngle, float avgSpeed)
     {
     int basicAngle = mBasicAngles[finishAx][finishRow];
-    return mRotation.finishRotationNow(listener,basicAngle,finishAngle,avgSpeed);
+    return mRotation.finishRotation(listener,finishAx,finishRow,basicAngle,finishAngle,avgSpeed);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1125,14 +1123,21 @@ public abstract class TwistyObject
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  synchronized long addNewRotation(EffectListener listener, int axis, int rowBitmap, int angle, long durationMillis )
+  synchronized long addRotation(EffectListener listener, int axis, int rowBitmap, int angle, long durationMillis )
+    {
+    return mRotation.addRotation(listener,mAxis[axis],axis,rowBitmap,angle,durationMillis);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enableGhostAxis(boolean enable)
     {
-    return mRotation.addNewRotation(listener,mAxis[axis],axis,rowBitmap,angle,durationMillis);
+    mTouchControl.enableGhostAxis(enable);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  synchronized boolean beginNewRotation(int axis, int row )
+  synchronized boolean beginRotation(int axis, int row )
     {
     if( axis<0 || axis>=mNumAxis )
       {
@@ -1145,7 +1150,7 @@ public abstract class TwistyObject
       return false;
       }
 
-    return mRotation.beginNewRotation(mAxis[axis],axis,row);
+    return mRotation.beginRotation(mAxis[axis],axis,row);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControl.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControl.java
index f2127c01..c2d996da 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControl.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControl.java
@@ -150,4 +150,5 @@ public abstract class TouchControl
   public abstract int getTouchedCubitFace();
   public abstract int getTouchedCubit();
   public abstract float[] getTouchedPuzzleCenter();
+  public abstract void enableGhostAxis(boolean enable);
   }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java
index d6fbe442..04ad1d86 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java
@@ -31,6 +31,7 @@ public class TouchControlBall extends TouchControl
   private final float[] mMove2D;
   private final float[][] mCastedRotAxis;
   private float[][] mTouchBorders;
+  private boolean mGhostAxisEnabled;
 
   private float mLongitude, mLatitude;
   private float mX, mY, mZ;
@@ -81,6 +82,8 @@ public class TouchControlBall extends TouchControl
     mEnabledRotAxis = new int[numRotAxis+1];
     mCastedRotAxis = new float[numRotAxis][2];
 
+    mGhostAxisEnabled = true;
+
     computeBorders(cuts,rotatable,size);
     }
 
@@ -212,11 +215,19 @@ public class TouchControlBall extends TouchControl
 
   private void computeEnabledAxis()
     {
-    int face = returnTouchedFace();
-    int part = returnTouchedPart();
-    int num = mEnabled[face][0].length;
-    mEnabledRotAxis[0] = num;
-    System.arraycopy(mEnabled[face][part], 0, mEnabledRotAxis, 1, num);
+    if( mGhostAxisEnabled )
+      {
+      int face = returnTouchedFace();
+      int part = returnTouchedPart();
+      int num = mEnabled[face][0].length;
+      mEnabledRotAxis[0] = num;
+      System.arraycopy(mEnabled[face][part], 0, mEnabledRotAxis, 1, num);
+      }
+    else
+      {
+      mEnabledRotAxis[0] = 1;  // if in 'ghost' mode, only the 0th axis is enabled.
+      mEnabledRotAxis[1] = 0;
+      }
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -350,4 +361,11 @@ public class TouchControlBall extends TouchControl
     {
     return false;
     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enableGhostAxis(boolean enable)
+    {
+    mGhostAxisEnabled = enable;
+    }
 }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlMirror.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlMirror.java
index 0bb5c345..f5a4dead 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlMirror.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlMirror.java
@@ -148,11 +148,18 @@ public class TouchControlMirror extends TouchControlShapeChanging
     float dy = mPoint[1] - rotatedTouchPoint.get1()/mObjectRatio;
     float dz = mPoint[2] - rotatedTouchPoint.get2()/mObjectRatio;
 
-    int disabledAxis = computeDisabledAxis();
-    int rotIndex     = computeRotationIndex(disabledAxis,dx,dy,dz);
-    int row          = computeRow(mTouchedCubit,rotIndex);
-
-    output[0] = rotIndex;
-    output[1] = row;
+    if( mGhostAxisEnabled )
+      {
+      int disabledAxis = computeDisabledAxis();
+      int rotIndex = computeRotationIndex(disabledAxis,dx,dy,dz);
+      int row = computeRow(mTouchedCubit,rotIndex);
+      output[0] = rotIndex;
+      output[1] = row;
+      }
+    else
+      {
+      output[0] = 0;
+      output[1] = computeRow(mTouchedCubit,0);
+      }
     }
   }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java
index b862c614..85382abc 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java
@@ -95,6 +95,7 @@ public class TouchControlShapeChanging extends TouchControl
   int mTouchedCubit, mTouchedFace, mNumAxis;
   FaceInfo[][] mInfos;
   float[][] mQuats;
+  boolean mGhostAxisEnabled;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -615,4 +616,11 @@ public class TouchControlShapeChanging extends TouchControl
     {
     return mTouchedCubit;
     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enableGhostAxis(boolean enable)
+    {
+    mGhostAxisEnabled = enable;
+    }
   }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java
index 3cf10a2a..58dfda4a 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java
@@ -35,6 +35,7 @@ public abstract class TouchControlShapeConstant extends TouchControl
 
   private final int mSplit;
   private final int[][][] mEnabled;
+  private boolean mGhostAxisEnabled;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -69,6 +70,8 @@ public abstract class TouchControlShapeConstant extends TouchControl
 
     mDistanceCenterFace3D = distance3D; // distance from the center of the object to each of its faces
 
+    mGhostAxisEnabled = true;
+
     computeCastedAxis(mRotAxis);
     computeBorders(cuts,rotatable,size);
     computeLinear(mRotAxis,faceAxis);
@@ -318,13 +321,21 @@ public abstract class TouchControlShapeConstant extends TouchControl
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  void computeEnabledAxis(int face, float[] touchPoint, int[] enabled)
+  void computeEnabledAxis(int face, float[] touchPoint)
     {
-    int part = returnPart(mSplit,face,touchPoint);
+    if( mGhostAxisEnabled )
+      {
+      int part = returnPart(mSplit, face, touchPoint);
 
-    int num = mEnabled[face][0].length;
-    enabled[0] = num;
-    System.arraycopy(mEnabled[face][part], 0, enabled, 1, num);
+      int num = mEnabled[face][0].length;
+      mEnabledRotAxis[0] = num;
+      System.arraycopy(mEnabled[face][part], 0, mEnabledRotAxis, 1, num);
+      }
+    else
+      {
+      mEnabledRotAxis[0] = 1;  // if in 'ghost' mode, only the 0th axis is enabled.
+      mEnabledRotAxis[1] = 0;
+      }
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -378,7 +389,7 @@ public abstract class TouchControlShapeConstant extends TouchControl
     mMove2D[0] -= mPoint2D[0];
     mMove2D[1] -= mPoint2D[1];
 
-    computeEnabledAxis(mLastTouchedFace, mPoint2D, mEnabledRotAxis);
+    computeEnabledAxis(mLastTouchedFace, mPoint2D);
     int rotIndex = computeRotationIndex( mCastedRotAxis[mLastTouchedFace], mMove2D, mEnabledRotAxis);
     float offset = computeOffset(mPoint2D, mCastedRotAxis[mLastTouchedFace][rotIndex]);
     int row      = computeRowFromOffset(mLastTouchedFace,rotIndex,offset);
@@ -452,4 +463,11 @@ public abstract class TouchControlShapeConstant extends TouchControl
     {
     return 0;
     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enableGhostAxis(boolean enable)
+    {
+    mGhostAxisEnabled = enable;
+    }
   }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapemod.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapemod.java
index 1e2bcb4d..8291b6ab 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapemod.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapemod.java
@@ -94,11 +94,18 @@ public class TouchControlShapemod extends TouchControlShapeChanging
     float dy = mPoint[1] - rotatedTouchPoint.get1()/mObjectRatio;
     float dz = mPoint[2] - rotatedTouchPoint.get2()/mObjectRatio;
 
-    int disabledAxis = computeDisabledAxis(quat);
-    int rotIndex     = computeRotationIndex(disabledAxis,dx,dy,dz);
-    int row          = computeRow(mTouchedCubit,rotIndex);
-
-    output[0] = rotIndex;
-    output[1] = row;
+    if( mGhostAxisEnabled )
+      {
+      int disabledAxis = computeDisabledAxis(quat);
+      int rotIndex = computeRotationIndex(disabledAxis,dx,dy,dz);
+      int row = computeRow(mTouchedCubit,rotIndex);
+      output[0] = rotIndex;
+      output[1] = row;
+      }
+    else
+      {
+      output[0] = 0;
+      output[1] = computeRow(mTouchedCubit,0);
+      }
     }
   }
