commit 9c2f0c9194a0a1b154561c3dcb6c840a11bb3c9d
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Sep 25 09:35:09 2020 +0100

    Rename some classes.

diff --git a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
index 422a65cc..b16870e8 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
@@ -38,7 +38,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.scores.RubikScores;
 import org.distorted.states.RubikState;
 import org.distorted.states.RubikStatePlay;
@@ -105,9 +105,9 @@ public class RubikDialogNewRecord extends AppCompatDialogFragment
           RubikStatePlay play = (RubikStatePlay) RubikState.PLAY.getStateClass();
           int object = play.getObject();
           int size   = play.getSize();
-          int sizeIndex = RubikObjectList.getSizeIndex(object,size);
+          int sizeIndex = ObjectList.getSizeIndex(object,size);
 
-          bundle.putInt("tab", RubikObjectList.pack(object,sizeIndex) );
+          bundle.putInt("tab", ObjectList.pack(object,sizeIndex) );
           bundle.putBoolean("submitting", true);
 
           RubikDialogScores scoresDiag = new RubikDialogScores();
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPattern.java b/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
index 74bd8393..12a917dc 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
@@ -41,7 +41,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.patterns.RubikPatternList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -94,9 +94,9 @@ public class RubikDialogPattern extends AppCompatDialogFragment
 
     for(int i=0; i< RubikPatternList.NUM_OBJECTS; i++)
       {
-      RubikObjectList list = RubikPatternList.getObject(i);
+      ObjectList list = RubikPatternList.getObject(i);
       int size             = RubikPatternList.getSize(i);
-      int sizeIndex        = RubikObjectList.getSizeIndex(list.ordinal(),size);
+      int sizeIndex        = ObjectList.getSizeIndex(list.ordinal(),size);
       int iconID           = list.getIconIDs()[sizeIndex];
 
       ImageView imageView = new ImageView(act);
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java b/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
index 7ead1624..da0a4234 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
@@ -28,7 +28,7 @@ import android.widget.FrameLayout;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.patterns.RubikPattern;
 import org.distorted.patterns.RubikPatternList;
 import org.distorted.states.RubikState;
@@ -94,7 +94,7 @@ public class RubikDialogPatternView extends FrameLayout
         RubikPattern pattern = RubikPattern.getInstance();
         int[][] moves = pattern.reInitialize(mTab, groupPosition, childPosition);
 
-        RubikObjectList list = RubikPatternList.getObject(mTab);
+        ObjectList list = RubikPatternList.getObject(mTab);
         int size             = RubikPatternList.getSize(mTab);
 
         ract.setupObject(list, size, moves);
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScores.java b/src/main/java/org/distorted/dialogs/RubikDialogScores.java
index a2081f83..8a0dda29 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScores.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScores.java
@@ -41,7 +41,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -104,17 +104,17 @@ public class RubikDialogScores extends AppCompatDialogFragment
     tabLayout.setupWithViewPager(viewPager);
 
     viewPager.setCurrentItem(curTab);
-    RubikObjectList list;
+    ObjectList list;
 
-    for (int object=0; object<RubikObjectList.NUM_OBJECTS; object++)
+    for (int object = 0; object< ObjectList.NUM_OBJECTS; object++)
       {
-      list = RubikObjectList.getObject(object);
+      list = ObjectList.getObject(object);
       int[] iconID = list.getIconIDs();
       int len = list.getSizes().length;
 
       for(int size=0; size<len; size++)
         {
-        int t = RubikObjectList.pack(object,size);
+        int t = ObjectList.pack(object,size);
         ImageView imageView = new ImageView(act);
         imageView.setImageResource(iconID[size]);
         TabLayout.Tab tab = tabLayout.getTabAt(t);
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
index 7c085e66..cca7bf93 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
@@ -32,7 +32,7 @@ import android.widget.LinearLayout;
 
 import org.distorted.scores.RubikScores;
 import org.distorted.scores.RubikScoresDownloader;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -108,9 +108,9 @@ class RubikDialogScoresPagerAdapter extends PagerAdapter implements RubikScoresD
 
     for(int i=0; i<mNumTabs; i++)
       {
-      int object   = RubikObjectList.unpackObject(i);
-      int sizeIndex= RubikObjectList.unpackSizeIndex(i);
-      maxTab[i]    = RubikObjectList.getMaxLevel(object, sizeIndex);
+      int object   = ObjectList.unpackObject(i);
+      int sizeIndex= ObjectList.unpackSizeIndex(i);
+      maxTab[i]    = ObjectList.getMaxLevel(object, sizeIndex);
       toDoTab[i]   = 0;
 
       toDo += maxTab[i];
@@ -200,7 +200,7 @@ class RubikDialogScoresPagerAdapter extends PagerAdapter implements RubikScoresD
     {
     mAct = act;
     mDialog = diag;
-    mNumTabs = RubikObjectList.getTotal();
+    mNumTabs = ObjectList.getTotal();
     mViews = new RubikDialogScoresView[mNumTabs];
     mViewPager = viewPager;
     mIsSubmitting = isSubmitting;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
index 678efea6..ada809d1 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
@@ -33,7 +33,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.scores.RubikScores;
 
 import static org.distorted.scores.RubikScoresDownloader.MAX_PLACES;
@@ -88,8 +88,8 @@ public class RubikDialogScoresView extends FrameLayout
     Resources res = act.getResources();
     String packageName = act.getPackageName();
 
-    int object   = RubikObjectList.unpackObject(tab);
-    int sizeIndex= RubikObjectList.unpackSizeIndex(tab);
+    int object   = ObjectList.unpackObject(tab);
+    int sizeIndex= ObjectList.unpackSizeIndex(tab);
     RubikScores scores = RubikScores.getInstance();
 
     boolean inserted = false;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
index edad8296..3b37f4cc 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
@@ -40,7 +40,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.scores.RubikScores;
 import org.distorted.states.RubikState;
 import org.distorted.states.RubikStatePlay;
@@ -148,10 +148,10 @@ public class RubikDialogSetName extends AppCompatDialogFragment
 
           int object = play.getObject();
           int size   = play.getSize();
-          int sizeIndex = RubikObjectList.getSizeIndex(object,size);
+          int sizeIndex = ObjectList.getSizeIndex(object,size);
 
           Bundle bundle = new Bundle();
-          bundle.putInt("tab", RubikObjectList.pack(object,sizeIndex) );
+          bundle.putInt("tab", ObjectList.pack(object,sizeIndex) );
           bundle.putBoolean("submitting", true);
 
           RubikDialogScores scores = new RubikDialogScores();
diff --git a/src/main/java/org/distorted/effects/objectchange/ObjectChangeEffect.java b/src/main/java/org/distorted/effects/objectchange/ObjectChangeEffect.java
index dac17f4d..3277e87f 100644
--- a/src/main/java/org/distorted/effects/objectchange/ObjectChangeEffect.java
+++ b/src/main/java/org/distorted/effects/objectchange/ObjectChangeEffect.java
@@ -25,7 +25,7 @@ import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 
 import java.lang.reflect.Method;
 
@@ -73,7 +73,7 @@ public abstract class ObjectChangeEffect extends BaseEffect implements EffectLis
   private int[] mEffectFinished;
   private boolean[] mPhaseActive;
 
-  RubikObject[] mObject;
+  TwistyObject[] mObject;
   DistortedScreen mScreen;
   Effect[][] mCubeEffects;
   int[][] mCubeEffectPosition;
@@ -93,7 +93,7 @@ public abstract class ObjectChangeEffect extends BaseEffect implements EffectLis
     mNodeEffectPosition = new int[NUM_PHASES][];
     mCubeEffects        = new Effect[NUM_PHASES][];
     mNodeEffects        = new Effect[NUM_PHASES][];
-    mObject             = new RubikObject[NUM_PHASES];
+    mObject             = new TwistyObject[NUM_PHASES];
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java b/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
index 6ef8e1fd..da634a1a 100644
--- a/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
+++ b/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
@@ -25,7 +25,7 @@ import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 
 import java.lang.reflect.Method;
 import java.util.Random;
@@ -74,7 +74,7 @@ public abstract class ScrambleEffect extends BaseEffect implements EffectListene
   private Random mRnd;
   private int mBasicAngle;
 
-  RubikObject mObject;
+  TwistyObject mObject;
   Effect[] mNodeEffects;
   int[] mNodeEffectPosition;
   Effect[] mCubeEffects;
diff --git a/src/main/java/org/distorted/effects/solve/SolveEffect.java b/src/main/java/org/distorted/effects/solve/SolveEffect.java
index 632f00c1..e3077b10 100644
--- a/src/main/java/org/distorted/effects/solve/SolveEffect.java
+++ b/src/main/java/org/distorted/effects/solve/SolveEffect.java
@@ -25,7 +25,7 @@ import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 
 import java.lang.reflect.Method;
 
@@ -69,7 +69,7 @@ public abstract class SolveEffect extends BaseEffect implements EffectListener
   private int[] mCubeEffectNumber, mNodeEffectNumber;
   private int mPhase;
 
-  RubikObject mObject;
+  TwistyObject mObject;
   DistortedScreen mScreen;
   Effect[][] mCubeEffects;
   int[][] mCubeEffectPosition;
diff --git a/src/main/java/org/distorted/effects/win/WinEffect.java b/src/main/java/org/distorted/effects/win/WinEffect.java
index 7fac333f..4265f901 100644
--- a/src/main/java/org/distorted/effects/win/WinEffect.java
+++ b/src/main/java/org/distorted/effects/win/WinEffect.java
@@ -25,7 +25,7 @@ import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 
 import java.lang.reflect.Method;
 
@@ -67,7 +67,7 @@ public abstract class WinEffect extends BaseEffect implements EffectListener
   private int mEffectReturned;
   private int mCubeEffectNumber, mNodeEffectNumber;
 
-  RubikObject mObject;
+  TwistyObject mObject;
   DistortedScreen mScreen;
   Effect[] mCubeEffects;
   int[] mCubeEffectPosition;
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index 5f448dd9..01034c09 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -37,10 +37,10 @@ import org.distorted.dialogs.RubikDialogPrivacy;
 import org.distorted.effects.BaseEffect;
 import org.distorted.library.main.DistortedLibrary;
 
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 import org.distorted.scores.RubikScores;
 import org.distorted.scores.RubikScoresDownloader;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.states.RubikState;
 import org.distorted.states.RubikStatePlay;
 
@@ -219,11 +219,11 @@ public class RubikActivity extends AppCompatActivity
       int object = play.getObject();
       int size   = play.getSize();
 
-      if( object>=0 && object<RubikObjectList.NUM_OBJECTS )
+      if( object>=0 && object< ObjectList.NUM_OBJECTS )
         {
-        RubikObjectList obj = RubikObjectList.getObject(object);
+        ObjectList obj = ObjectList.getObject(object);
         int[] sizes = obj.getSizes();
-        int sizeIndex = RubikObjectList.getSizeIndex(object,size);
+        int sizeIndex = ObjectList.getSizeIndex(object,size);
 
         if( sizeIndex>=0 && sizeIndex<sizes.length )
           {
@@ -234,7 +234,7 @@ public class RubikActivity extends AppCompatActivity
 
       if( !success )
         {
-        RubikObjectList obj = RubikObjectList.getObject(RubikStatePlay.DEF_OBJECT);
+        ObjectList obj = ObjectList.getObject(RubikStatePlay.DEF_OBJECT);
         int s = RubikStatePlay.DEF_SIZE;
 
         play.setObjectAndSize(this,obj,s);
@@ -334,7 +334,7 @@ public class RubikActivity extends AppCompatActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public RubikObject getObject()
+    public TwistyObject getObject()
       {
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       RubikPreRender pre = view.getPreRender();
@@ -365,15 +365,15 @@ public class RubikActivity extends AppCompatActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void changeObject(RubikObjectList newObject, int newSize, boolean reportChange)
+    public void changeObject(ObjectList newObject, int newSize, boolean reportChange)
       {
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       RubikPreRender pre = view.getPreRender();
 
       if( reportChange )
         {
-        RubikObject oldObject = pre.getObject();
-        RubikObjectList oldList = oldObject.getObjectList();
+        TwistyObject oldObject = pre.getObject();
+        ObjectList oldList = oldObject.getObjectList();
         int oldSize = oldObject.getSize();
         float fps = view.getRenderer().getFPS();
         fps = (int)(fps+0.5f);
@@ -410,7 +410,7 @@ public class RubikActivity extends AppCompatActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void setupObject(RubikObjectList object, int size, int[][] moves)
+    public void setupObject(ObjectList object, int size, int[][] moves)
       {
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       RubikPreRender pre = view.getPreRender();
diff --git a/src/main/java/org/distorted/main/RubikPreRender.java b/src/main/java/org/distorted/main/RubikPreRender.java
index 52c9da55..32b9e183 100644
--- a/src/main/java/org/distorted/main/RubikPreRender.java
+++ b/src/main/java/org/distorted/main/RubikPreRender.java
@@ -28,8 +28,8 @@ import org.distorted.dialogs.RubikDialogNewRecord;
 import org.distorted.dialogs.RubikDialogSolved;
 import org.distorted.effects.BaseEffect;
 import org.distorted.library.message.EffectListener;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.objects.ObjectList;
 import org.distorted.scores.RubikScores;
 import org.distorted.states.RubikState;
 import org.distorted.states.RubikStateSolving;
@@ -49,7 +49,7 @@ public class RubikPreRender implements EffectListener
                   mInitializeObject, mSetTextureMap, mResetAllTextureMaps;
   private boolean mCanRotate, mCanPlay;
   private boolean mIsSolved;
-  private RubikObjectList mNextObject;
+  private ObjectList mNextObject;
   private int mNextSize;
   private long mRotationFinishedID;
   private long[] mEffectID;
@@ -58,7 +58,7 @@ public class RubikPreRender implements EffectListener
   private int mScreenWidth, mScreenHeight;
   private SharedPreferences mPreferences;
   private int[][] mNextMoves;
-  private RubikObject mOldObject, mNewObject;
+  private TwistyObject mOldObject, mNewObject;
   private int mScrambleObjectNum;
   private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
   private long mAddRotationDuration;
@@ -97,7 +97,7 @@ public class RubikPreRender implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void createObjectNow(RubikObjectList object, int size, int[][] moves)
+  private void createObjectNow(ObjectList object, int size, int[][] moves)
     {
     boolean firstTime = (mNewObject==null);
 
@@ -366,7 +366,7 @@ public class RubikPreRender implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  void changeObject(RubikObjectList object, int size)
+  void changeObject(ObjectList object, int size)
     {
     if( size>0 )
       {
@@ -378,7 +378,7 @@ public class RubikPreRender implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  void setupObject(RubikObjectList object, int size, int[][] moves)
+  void setupObject(ObjectList object, int size, int[][] moves)
     {
     if( size>0 )
       {
@@ -492,14 +492,14 @@ public class RubikPreRender implements EffectListener
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public RubikObject getObject()
+  public TwistyObject getObject()
     {
     return mNewObject;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public RubikObject getOldObject()
+  public TwistyObject getOldObject()
     {
     return mOldObject;
     }
diff --git a/src/main/java/org/distorted/main/RubikSurfaceView.java b/src/main/java/org/distorted/main/RubikSurfaceView.java
index e22e4578..375d50b9 100644
--- a/src/main/java/org/distorted/main/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/main/RubikSurfaceView.java
@@ -32,7 +32,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
 import org.distorted.library.type.Static2D;
 import org.distorted.library.type.Static4D;
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 import org.distorted.objects.Movement;
 import org.distorted.solvers.SolverMain;
 import org.distorted.states.RubikState;
@@ -328,7 +328,7 @@ public class RubikSurfaceView extends GLSurfaceView
               mLastCubitFace = mMovement.getTouchedFace();
               float[] point = mMovement.getTouchedPoint3D();
               int color = solver.getCurrentColor();
-              RubikObject object = mPreRender.getObject();
+              TwistyObject object = mPreRender.getObject();
               mLastCubit = object.getCubit(point);
               mPreRender.setTextureMap( mLastCubit, mLastCubitFace, color );
               mLastCubitColor = SolverMain.cubitIsLocked(object.getObjectList(), object.getSize(), mLastCubit);
@@ -389,7 +389,7 @@ public class RubikSurfaceView extends GLSurfaceView
         float distQuot = mInitDistance<0 ? 1.0f : distNow/ mInitDistance;
         mInitDistance = distNow;
 
-        RubikObject object = mPreRender.getObject();
+        TwistyObject object = mPreRender.getObject();
         if( object!=null ) object.setObjectRatio(distQuot);
         }
       else
@@ -463,7 +463,7 @@ public class RubikSurfaceView extends GLSurfaceView
       Static4D rotatedTouchPoint2= rotateVectorByInvertedQuat(touchPoint2, mQuat);
 
       Static2D res = mMovement.newRotation(rotatedTouchPoint2);
-      RubikObject object = mPreRender.getObject();
+      TwistyObject object = mPreRender.getObject();
 
       mCurrentAxis = (int)res.get0();
       float offset = res.get1();
diff --git a/src/main/java/org/distorted/objects/Cubit.java b/src/main/java/org/distorted/objects/Cubit.java
index f3bbeada..e893c1bc 100644
--- a/src/main/java/org/distorted/objects/Cubit.java
+++ b/src/main/java/org/distorted/objects/Cubit.java
@@ -31,7 +31,7 @@ class Cubit
   {
   private final Static3D mOrigPosition;
 
-  private RubikObject mParent;
+  private TwistyObject mParent;
   private Static3D mCurrentPosition;
   private int mNumAxis;
 
@@ -40,7 +40,7 @@ class Cubit
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Cubit(RubikObject parent, Static3D position)
+  Cubit(TwistyObject parent, Static3D position)
     {
     float x = position.get0();
     float y = position.get1();
@@ -167,7 +167,7 @@ class Cubit
       {
       row = (int)(mRotationRow[axis]+0.5f);
       result += (1<<(row+accumulativeShift));
-      accumulativeShift += RubikObjectList.MAX_OBJECT_SIZE;
+      accumulativeShift += ObjectList.MAX_OBJECT_SIZE;
       }
 
     return result;
diff --git a/src/main/java/org/distorted/objects/Movement.java b/src/main/java/org/distorted/objects/Movement.java
index 88e8dfcd..02733a3c 100644
--- a/src/main/java/org/distorted/objects/Movement.java
+++ b/src/main/java/org/distorted/objects/Movement.java
@@ -240,7 +240,7 @@ public abstract class Movement
 
   public boolean faceTouched(Static4D rotatedTouchPoint, Static4D rotatedCamera)
     {
-    float objectRatio = RubikObject.getObjectRatio();
+    float objectRatio = TwistyObject.getObjectRatio();
 
     mPoint[0]  = rotatedTouchPoint.get0()/objectRatio;
     mPoint[1]  = rotatedTouchPoint.get1()/objectRatio;
@@ -267,7 +267,7 @@ public abstract class Movement
 
   public Static2D newRotation(Static4D rotatedTouchPoint)
     {
-    float objectRatio = RubikObject.getObjectRatio();
+    float objectRatio = TwistyObject.getObjectRatio();
 
     mPoint[0] = rotatedTouchPoint.get0()/objectRatio;
     mPoint[1] = rotatedTouchPoint.get1()/objectRatio;
diff --git a/src/main/java/org/distorted/objects/MovementCube.java b/src/main/java/org/distorted/objects/MovementCube.java
index 0af6c6cf..ed96119e 100644
--- a/src/main/java/org/distorted/objects/MovementCube.java
+++ b/src/main/java/org/distorted/objects/MovementCube.java
@@ -25,7 +25,7 @@ class MovementCube extends Movement
 {
   MovementCube()
     {
-    super(RubikCube.ROT_AXIS, RubikCube.FACE_AXIS, 0.5f, 0.5f);
+    super(TwistyCube.ROT_AXIS, TwistyCube.FACE_AXIS, 0.5f, 0.5f);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/MovementDiamond.java b/src/main/java/org/distorted/objects/MovementDiamond.java
index 879c6a09..0a5e5b68 100644
--- a/src/main/java/org/distorted/objects/MovementDiamond.java
+++ b/src/main/java/org/distorted/objects/MovementDiamond.java
@@ -25,7 +25,7 @@ class MovementDiamond extends Movement
 {
   MovementDiamond()
     {
-    super(RubikDiamond.ROT_AXIS, RubikDiamond.FACE_AXIS, 0.25f, 0.25f);
+    super(TwistyDiamond.ROT_AXIS, TwistyDiamond.FACE_AXIS, 0.25f, 0.25f);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/MovementDino.java b/src/main/java/org/distorted/objects/MovementDino.java
index ea60bbdc..88dd98ab 100644
--- a/src/main/java/org/distorted/objects/MovementDino.java
+++ b/src/main/java/org/distorted/objects/MovementDino.java
@@ -25,7 +25,7 @@ class MovementDino extends Movement
 {
   MovementDino()
     {
-    super(RubikDino.ROT_AXIS, RubikDino.FACE_AXIS, 0.5f, 0.5f);
+    super(TwistyDino.ROT_AXIS, TwistyDino.FACE_AXIS, 0.5f, 0.5f);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/MovementHelicopter.java b/src/main/java/org/distorted/objects/MovementHelicopter.java
index b92f494c..5f2e7782 100644
--- a/src/main/java/org/distorted/objects/MovementHelicopter.java
+++ b/src/main/java/org/distorted/objects/MovementHelicopter.java
@@ -25,7 +25,7 @@ class MovementHelicopter extends Movement
 {
   MovementHelicopter()
     {
-    super(RubikHelicopter.ROT_AXIS, RubikHelicopter.FACE_AXIS, 0.166f, 0.166f);
+    super(TwistyHelicopter.ROT_AXIS, TwistyHelicopter.FACE_AXIS, 0.166f, 0.166f);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/MovementPyraminx.java b/src/main/java/org/distorted/objects/MovementPyraminx.java
index 49430de6..e608d85f 100644
--- a/src/main/java/org/distorted/objects/MovementPyraminx.java
+++ b/src/main/java/org/distorted/objects/MovementPyraminx.java
@@ -30,7 +30,7 @@ class MovementPyraminx extends Movement
 
   MovementPyraminx()
     {
-    super(RubikPyraminx.ROT_AXIS, RubikPyraminx.FACE_AXIS, SQ6/12, SQ3/6);
+    super(TwistyPyraminx.ROT_AXIS, TwistyPyraminx.FACE_AXIS, SQ6/12, SQ3/6);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/MovementSkewb.java b/src/main/java/org/distorted/objects/MovementSkewb.java
index a4a78132..b3e46995 100644
--- a/src/main/java/org/distorted/objects/MovementSkewb.java
+++ b/src/main/java/org/distorted/objects/MovementSkewb.java
@@ -25,7 +25,7 @@ class MovementSkewb extends Movement
 {
   MovementSkewb()
     {
-    super(RubikSkewb.ROT_AXIS, RubikSkewb.FACE_AXIS, 0.25f, 0.25f);
+    super(TwistySkewb.ROT_AXIS, TwistySkewb.FACE_AXIS, 0.25f, 0.25f);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/ObjectList.java b/src/main/java/org/distorted/objects/ObjectList.java
new file mode 100644
index 00000000..aaf111f0
--- /dev/null
+++ b/src/main/java/org/distorted/objects/ObjectList.java
@@ -0,0 +1,494 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+
+import java.lang.reflect.Field;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum ObjectList
+  {
+  CUBE (
+         new int[][] {
+                       {2 , 12, R.raw.cube2, R.drawable.ui_small_cube2, R.drawable.ui_medium_cube2, R.drawable.ui_big_cube2, R.drawable.ui_huge_cube2} ,
+                       {3 , 16, R.raw.cube3, R.drawable.ui_small_cube3, R.drawable.ui_medium_cube3, R.drawable.ui_big_cube3, R.drawable.ui_huge_cube3} ,
+                       {4 , 20, R.raw.cube4, R.drawable.ui_small_cube4, R.drawable.ui_medium_cube4, R.drawable.ui_big_cube4, R.drawable.ui_huge_cube4} ,
+                       {5 , 24, R.raw.cube5, R.drawable.ui_small_cube5, R.drawable.ui_medium_cube5, R.drawable.ui_big_cube5, R.drawable.ui_huge_cube5}
+                     },
+         TwistyCube.class,
+         new MovementCube(),
+         0
+       ),
+
+  PYRA (
+         new int[][] {
+                       {3 , 10, R.raw.pyra3, R.drawable.ui_small_pyra3, R.drawable.ui_medium_pyra3, R.drawable.ui_big_pyra3, R.drawable.ui_huge_pyra3} ,
+                       {4 , 15, R.raw.pyra4, R.drawable.ui_small_pyra4, R.drawable.ui_medium_pyra4, R.drawable.ui_big_pyra4, R.drawable.ui_huge_pyra4} ,
+                       {5 , 20, R.raw.pyra5, R.drawable.ui_small_pyra5, R.drawable.ui_medium_pyra5, R.drawable.ui_big_pyra5, R.drawable.ui_huge_pyra5}
+                     },
+         TwistyPyraminx.class,
+         new MovementPyraminx(),
+         1
+       ),
+
+  DIAM (
+         new int[][] {
+                       {2 , 10, R.raw.dino, R.drawable.ui_small_dino, R.drawable.ui_medium_dino, R.drawable.ui_big_dino, R.drawable.ui_huge_dino} ,
+                     },
+         TwistyDiamond.class,
+         new MovementDiamond(),
+         1
+       ),
+
+  DINO (
+         new int[][] {
+                       {3 , 10, R.raw.dino, R.drawable.ui_small_dino, R.drawable.ui_medium_dino, R.drawable.ui_big_dino, R.drawable.ui_huge_dino} ,
+                     },
+         TwistyDino6.class,
+         new MovementDino(),
+         2
+       ),
+
+  DIN4 (
+         new int[][] {
+                       {3 ,  7, R.raw.dino, R.drawable.ui_small_din4, R.drawable.ui_medium_din4, R.drawable.ui_big_din4, R.drawable.ui_huge_din4} ,
+                     },
+         TwistyDino4.class,
+         new MovementDino(),
+         2
+       ),
+
+  SKEW (
+         new int[][] {
+                       {2 , 11, R.raw.skewb, R.drawable.ui_small_skewb, R.drawable.ui_medium_skewb, R.drawable.ui_big_skewb, R.drawable.ui_huge_skewb} ,
+                     },
+         TwistySkewb.class,
+         new MovementSkewb(),
+         2
+       ),
+
+  HELI (
+         new int[][] {
+                       {3 , 18, R.raw.heli, R.drawable.ui_small_heli, R.drawable.ui_medium_heli, R.drawable.ui_big_heli, R.drawable.ui_huge_heli} ,
+                     },
+         TwistyHelicopter.class,
+         new MovementHelicopter(),
+         2
+       ),
+  ;
+
+  public static final int NUM_OBJECTS = values().length;
+  public static final int MAX_NUM_OBJECTS;
+  public static final int MAX_LEVEL;
+  public static final int MAX_OBJECT_SIZE;
+
+  private final int[] mObjectSizes, mMaxLevels, mSmallIconIDs, mMediumIconIDs, mBigIconIDs, mHugeIconIDs, mResourceIDs;
+  private final Class<? extends TwistyObject> mObjectClass;
+  private final Movement mObjectMovementClass;
+  private final int mColumn, mNumSizes;
+
+  private static final ObjectList[] objects;
+  private static int mNumAll;
+  private static int[] mIndices;
+  private static int mColCount, mRowCount;
+
+  static
+    {
+    mNumAll = 0;
+    int num, i = 0;
+    objects = new ObjectList[NUM_OBJECTS];
+    int maxNum  = Integer.MIN_VALUE;
+    int maxLevel= Integer.MIN_VALUE;
+    int maxSize = Integer.MIN_VALUE;
+
+    for(ObjectList object: ObjectList.values())
+      {
+      objects[i] = object;
+      i++;
+      num = object.mObjectSizes.length;
+      mNumAll += num;
+      if( num> maxNum ) maxNum = num;
+
+      for(int j=0; j<num; j++)
+        {
+        if( object.mMaxLevels[j] > maxLevel ) maxLevel = object.mMaxLevels[j];
+        if( object.mObjectSizes[j] > maxSize) maxSize  = object.mObjectSizes[j];
+        }
+      }
+
+    MAX_NUM_OBJECTS = maxNum;
+    MAX_LEVEL       = maxLevel;
+    MAX_OBJECT_SIZE = maxSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void setUpColAndRow()
+    {
+    mIndices = new int[NUM_OBJECTS];
+    mColCount= 0;
+
+    for(int obj=0; obj<NUM_OBJECTS; obj++)
+      {
+      mIndices[obj] = objects[obj].mColumn;
+      if( mIndices[obj]>=mColCount ) mColCount = mIndices[obj]+1;
+      }
+
+    mRowCount = 0;
+
+    for(int col=0; col<mColCount; col++)
+      {
+      int numObjects = computeNumObjectsInColumn(col);
+      if( numObjects>mRowCount ) mRowCount = numObjects;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int computeNumObjectsInColumn(int column)
+    {
+    int num=0;
+
+    for(int object=0; object<NUM_OBJECTS; object++)
+      {
+      if( objects[object].mColumn == column )
+        {
+        num += objects[object].mNumSizes;
+        }
+      }
+
+    return num;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getColumnCount()
+    {
+    if( mIndices==null ) setUpColAndRow();
+
+    return mColCount;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getRowCount()
+    {
+    if( mIndices==null ) setUpColAndRow();
+
+    return mRowCount;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int[] getIndices()
+    {
+    if( mIndices==null ) setUpColAndRow();
+
+    return mIndices;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ObjectList getObject(int ordinal)
+    {
+    return ordinal>=0 && ordinal<NUM_OBJECTS ? objects[ordinal] : CUBE;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int pack(int object, int sizeIndex)
+    {
+    int ret = 0;
+    for(int i=0; i<object; i++) ret += objects[i].mObjectSizes.length;
+
+    return ret+sizeIndex;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int unpackSizeIndex(int number)
+    {
+    int num;
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      {
+      num = objects[i].mObjectSizes.length;
+      if( number<num ) return number;
+      number -= num;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int unpackObject(int number)
+    {
+    int num;
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      {
+      num = objects[i].mObjectSizes.length;
+      if( number<num ) return i;
+      number -= num;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int unpackObjectFromString(String obj)
+    {
+    int u = obj.indexOf('_');
+    int l = obj.length();
+
+    if( u>0 )
+      {
+      String name = obj.substring(0,u);
+      int size = Integer.parseInt( obj.substring(u+1,l) );
+
+      for(int i=0; i<NUM_OBJECTS; i++)
+        {
+        if( objects[i].name().equals(name) )
+          {
+          int sizeIndex = getSizeIndex(i,size);
+          return pack(i,sizeIndex);
+          }
+        }
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static String getObjectList()
+    {
+    String name;
+    StringBuilder list = new StringBuilder();
+    int len;
+    int[] sizes;
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      {
+      sizes = objects[i].mObjectSizes;
+      len   = sizes.length;
+      name  = objects[i].name();
+
+      for(int j=0; j<len; j++)
+        {
+        if( i>0 || j>0 ) list.append(',');
+        list.append(name);
+        list.append('_');
+        list.append(sizes[j]);
+        }
+      }
+
+    return list.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getTotal()
+    {
+    return mNumAll;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getMaxLevel(int ordinal, int sizeIndex)
+    {
+    if( ordinal>=0 && ordinal<NUM_OBJECTS )
+      {
+      int num = objects[ordinal].mObjectSizes.length;
+      return sizeIndex>=0 && sizeIndex<num ? objects[ordinal].mMaxLevels[sizeIndex] : 0;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getOrdinal(String name)
+    {
+    for(int i=0; i<NUM_OBJECTS; i++)
+      {
+      if(objects[i].name().equals(name)) return i;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getSizeIndex(int ordinal, int size)
+    {
+    if( ordinal>=0 && ordinal<NUM_OBJECTS )
+      {
+      int[] sizes = objects[ordinal].getSizes();
+      int len = sizes.length;
+
+      for(int i=0; i<len; i++)
+        {
+        if( sizes[i]==size ) return i;
+        }
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int[] retFaceColors(ObjectList object)
+    {
+    Field field;
+    int[] faceColors=null;
+
+    try
+      {
+      field = object.mObjectClass.getDeclaredField("FACE_COLORS");
+      field.setAccessible(true);
+      Object obj = field.get(null);
+      faceColors = (int[]) obj;
+      }
+    catch(NoSuchFieldException ex)
+      {
+      android.util.Log.e("RubikObjectList", object.mObjectClass.getSimpleName()+": no such field exception getting field: "+ex.getMessage());
+      }
+    catch(IllegalAccessException ex)
+      {
+      android.util.Log.e("RubikObjectList", object.mObjectClass.getSimpleName()+": illegal access exception getting field: "+ex.getMessage());
+      }
+
+    return faceColors;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ObjectList(int[][] info, Class<? extends TwistyObject> object , Movement movement, int column)
+    {
+    mNumSizes = info.length;
+
+    mObjectSizes  = new int[mNumSizes];
+    mMaxLevels    = new int[mNumSizes];
+    mResourceIDs  = new int[mNumSizes];
+    mSmallIconIDs = new int[mNumSizes];
+    mMediumIconIDs= new int[mNumSizes];
+    mBigIconIDs   = new int[mNumSizes];
+    mHugeIconIDs  = new int[mNumSizes];
+
+    for(int i=0; i<mNumSizes; i++)
+      {
+      mObjectSizes[i]  = info[i][0];
+      mMaxLevels[i]    = info[i][1];
+      mResourceIDs[i]  = info[i][2];
+      mSmallIconIDs[i] = info[i][3];
+      mMediumIconIDs[i]= info[i][4];
+      mBigIconIDs[i]   = info[i][5];
+      mHugeIconIDs[i]  = info[i][6];
+      }
+
+    mObjectClass         = object;
+    mObjectMovementClass = movement;
+    mColumn              = column;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getSizes()
+    {
+    return mObjectSizes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getMaxLevels()
+    {
+    return mMaxLevels;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getIconIDs()
+    {
+    int size = RubikActivity.getDrawableSize();
+
+    switch(size)
+      {
+      case 0 : return mSmallIconIDs;
+      case 1 : return mMediumIconIDs;
+      case 2 : return mBigIconIDs;
+      default: return mHugeIconIDs;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getResourceIDs()
+    {
+    return mResourceIDs;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumVariants()
+    {
+    return mObjectSizes.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public TwistyObject create(int size, Static4D quat, int[][] moves, Resources res, int scrWidth)
+    {
+    DistortedTexture texture = new DistortedTexture();
+    DistortedEffects effects = new DistortedEffects();
+    MeshSquare mesh          = new MeshSquare(20,20);   // mesh of the node, not of the cubits
+
+    switch(ordinal())
+      {
+      case 0: return new TwistyCube(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 1: return new TwistyPyraminx(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 2: return new TwistyDiamond(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 3: return new TwistyDino6(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 4: return new TwistyDino4(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 5: return new TwistySkewb(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 6: return new TwistyHelicopter(size, quat, texture, mesh, effects, moves, res, scrWidth);
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Movement getObjectMovementClass()
+    {
+    return mObjectMovementClass;
+    }
+  }
diff --git a/src/main/java/org/distorted/objects/RubikCube.java b/src/main/java/org/distorted/objects/RubikCube.java
deleted file mode 100644
index 9d57b55a..00000000
--- a/src/main/java/org/distorted/objects/RubikCube.java
+++ /dev/null
@@ -1,658 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2019 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.objects;
-
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.VertexEffectDeform;
-import org.distorted.library.effect.VertexEffectMove;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshPolygon;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.RubikSurfaceView;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class RubikCube extends RubikObject
-{
-  static final float SQ2 = (float)Math.sqrt(2);
-
-  // the three rotation axis of a RubikCube. Must be normalized.
-  static final Static3D[] ROT_AXIS = new Static3D[]
-         {
-           new Static3D(1,0,0),
-           new Static3D(0,1,0),
-           new Static3D(0,0,1)
-         };
-
-  // the six axis that determine the faces
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(1,0,0), new Static3D(-1,0,0),
-           new Static3D(0,1,0), new Static3D(0,-1,0),
-           new Static3D(0,0,1), new Static3D(0,0,-1)
-         };
-
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_YELLOW, COLOR_WHITE,
-           COLOR_BLUE  , COLOR_GREEN,
-           COLOR_RED   , COLOR_BROWN
-         };
-
-  // All legal rotation quats of a RubikCube of any size.
-  // Here's how to compute this:
-  // 1) compute how many rotations there are (RubikCube of any size = 24)
-  // 2) take the AXIS, angles of rotation (90 in RubikCube's case) compute the basic quaternions
-  // (i.e. rotations of 1 basic angle along each of the axis) and from there start semi-randomly
-  // multiplying them and eventually you'll find all (24) legal rotations.
-  // Example program in C, res/raw/compute_quats.c , is included.
-  private static final Static4D[] QUATS = new Static4D[]
-         {
-         new Static4D(  0.0f,   0.0f,   0.0f,   1.0f),
-         new Static4D(  1.0f,   0.0f,   0.0f,   0.0f),
-         new Static4D(  0.0f,   1.0f,   0.0f,   0.0f),
-         new Static4D(  0.0f,   0.0f,   1.0f,   0.0f),
-
-         new Static4D( SQ2/2,  SQ2/2,  0.0f ,   0.0f),
-         new Static4D( SQ2/2, -SQ2/2,  0.0f ,   0.0f),
-         new Static4D( SQ2/2,   0.0f,  SQ2/2,   0.0f),
-         new Static4D(-SQ2/2,   0.0f,  SQ2/2,   0.0f),
-         new Static4D( SQ2/2,   0.0f,   0.0f,  SQ2/2),
-         new Static4D( SQ2/2,   0.0f,   0.0f, -SQ2/2),
-         new Static4D(  0.0f,  SQ2/2,  SQ2/2,   0.0f),
-         new Static4D(  0.0f,  SQ2/2, -SQ2/2,   0.0f),
-         new Static4D(  0.0f,  SQ2/2,   0.0f,  SQ2/2),
-         new Static4D(  0.0f,  SQ2/2,   0.0f, -SQ2/2),
-         new Static4D(  0.0f,   0.0f,  SQ2/2,  SQ2/2),
-         new Static4D(  0.0f,   0.0f,  SQ2/2, -SQ2/2),
-
-         new Static4D(  0.5f,   0.5f,   0.5f,   0.5f),
-         new Static4D(  0.5f,   0.5f,  -0.5f,   0.5f),
-         new Static4D(  0.5f,   0.5f,  -0.5f,  -0.5f),
-         new Static4D(  0.5f,  -0.5f,   0.5f,  -0.5f),
-         new Static4D( -0.5f,  -0.5f,  -0.5f,   0.5f),
-         new Static4D( -0.5f,   0.5f,  -0.5f,  -0.5f),
-         new Static4D( -0.5f,   0.5f,   0.5f,  -0.5f),
-         new Static4D( -0.5f,   0.5f,   0.5f,   0.5f)
-         };
-
-  private static MeshBase[] mMeshes;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikCube(int size, Static4D quat, DistortedTexture texture,
-            MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, 60, quat, texture, mesh, effects, moves, RubikObjectList.CUBE, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// paint the square with upper-right corner at (left,top) and side length 'side' with texture
-// for face 'face'.
-
-  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
-    {
-    final float R = side*0.10f;
-    final float M = side*0.05f;
-
-    paint.setColor(FACE_COLORS[face]);
-    canvas.drawRoundRect( left+M, top+M, left+side-M, top+side-M, R, R, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static3D[] getCubitPositions(int size)
-    {
-    int numCubits = size>1 ? 6*size*size - 12*size + 8 : 1;
-    Static3D[] tmp = new Static3D[numCubits];
-
-    float diff = 0.5f*(size-1);
-    int currentPosition = 0;
-
-    for(int x = 0; x<size; x++)
-      for(int y = 0; y<size; y++)
-        for(int z = 0; z<size; z++)
-          if( x==0 || x==size-1 || y==0 || y==size-1 || z==0 || z==size-1 )
-            {
-            tmp[currentPosition++] = new Static3D(x-diff,y-diff,z-diff);
-            }
-
-    return tmp;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static4D[] getQuats()
-    {
-    return QUATS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getBasicStep()
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumStickerTypes()
-    {
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumCubitFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.5f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    boolean belongs = isOnFace(cubit, cubitface/2, cubitface%2==0 ? size-1:0 );
-    return belongs ? cubitface : NUM_FACES;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int cubit)
-    {
-    int size   = getSize();
-    int ordinal= RubikObjectList.CUBE.ordinal();
-    int index  = RubikObjectList.getSizeIndex(ordinal,size);
-    float[] bands;
-    float D = 0.027f;
-    float E = 0.5f-D;
-    float[] vertices = { -E,-E, +E,-E, +E,+E, -E,+E };
-    int extraI, extraV;
-
-    switch(size)
-      {
-      case 2 : bands = new float[] { 1.0f    ,-D,
-                                     1.0f-D/2,-D*0.55f,
-                                     1.0f-D  ,-D*0.25f,
-                                     1.0f-2*D, 0.0f,
-                                     0.50f, 0.040f,
-                                     0.0f, 0.048f };
-               extraI = 2;
-               extraV = 2;
-               break;
-      case 3 : bands = new float[] { 1.0f    ,-D,
-                                     1.0f-D*1.2f,-D*0.55f,
-                                     1.0f-2*D, 0.0f,
-                                     0.50f, 0.040f,
-                                     0.0f, 0.048f };
-               extraI = 2;
-               extraV = 2;
-               break;
-      case 4 : bands = new float[] { 1.0f    ,-D,
-                                     1.0f-D*1.2f,-D*0.55f,
-                                     1.0f-2*D, 0.0f,
-                                     0.50f, 0.040f,
-                                     0.0f, 0.048f };
-               extraI = 1;
-               extraV = 2;
-               break;
-      default: bands = new float[] { 1.0f    ,-D,
-                                     1.0f-2*D, 0.0f,
-                                     0.50f, 0.025f,
-                                     0.0f, 0.030f };
-               extraI = 1;
-               extraV = 1;
-               break;
-      }
-
-    return createCubitMesh(index,vertices,bands,extraI,extraV);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int index, float[] vertices, float[] bands, int extraI, int extraV)
-    {
-    if( mMeshes==null )
-      {
-      mMeshes = new MeshBase[RubikObjectList.CUBE.getNumVariants()];
-      }
-
-    if( mMeshes[index]==null )
-      {
-      final int MESHES=6;
-      int association = 1;
-      MeshBase[] meshes = new MeshPolygon[MESHES];
-      meshes[0] = new MeshPolygon(vertices,bands,extraI,extraV);
-      meshes[0].setEffectAssociation(0,association,0);
-
-      for(int i=1; i<MESHES; i++)
-        {
-        association <<=1;
-        meshes[i] = meshes[0].copy(true);
-        meshes[i].setEffectAssociation(0,association,0);
-        }
-
-      mMeshes[index] = new MeshJoined(meshes);
-
-      Static3D axisY   = new Static3D(0,1,0);
-      Static3D axisX   = new Static3D(1,0,0);
-      Static3D center  = new Static3D(0,0,0);
-      Static1D angle90 = new Static1D(90);
-      Static1D angle180= new Static1D(180);
-      Static1D angle270= new Static1D(270);
-
-      float d1 = 1.0f;
-      float d2 =-0.05f;
-      float d3 = 0.12f;
-
-      Static3D dCen0 = new Static3D( d1*(+0.5f), d1*(+0.5f), d1*(+0.5f) );
-      Static3D dCen1 = new Static3D( d1*(+0.5f), d1*(+0.5f), d1*(-0.5f) );
-      Static3D dCen2 = new Static3D( d1*(+0.5f), d1*(-0.5f), d1*(+0.5f) );
-      Static3D dCen3 = new Static3D( d1*(+0.5f), d1*(-0.5f), d1*(-0.5f) );
-      Static3D dCen4 = new Static3D( d1*(-0.5f), d1*(+0.5f), d1*(+0.5f) );
-      Static3D dCen5 = new Static3D( d1*(-0.5f), d1*(+0.5f), d1*(-0.5f) );
-      Static3D dCen6 = new Static3D( d1*(-0.5f), d1*(-0.5f), d1*(+0.5f) );
-      Static3D dCen7 = new Static3D( d1*(-0.5f), d1*(-0.5f), d1*(-0.5f) );
-
-      Static3D dVec0 = new Static3D( d2*(+0.5f), d2*(+0.5f), d2*(+0.5f) );
-      Static3D dVec1 = new Static3D( d2*(+0.5f), d2*(+0.5f), d2*(-0.5f) );
-      Static3D dVec2 = new Static3D( d2*(+0.5f), d2*(-0.5f), d2*(+0.5f) );
-      Static3D dVec3 = new Static3D( d2*(+0.5f), d2*(-0.5f), d2*(-0.5f) );
-      Static3D dVec4 = new Static3D( d2*(-0.5f), d2*(+0.5f), d2*(+0.5f) );
-      Static3D dVec5 = new Static3D( d2*(-0.5f), d2*(+0.5f), d2*(-0.5f) );
-      Static3D dVec6 = new Static3D( d2*(-0.5f), d2*(-0.5f), d2*(+0.5f) );
-      Static3D dVec7 = new Static3D( d2*(-0.5f), d2*(-0.5f), d2*(-0.5f) );
-
-      Static4D dReg  = new Static4D(0,0,0,d3);
-      Static1D dRad  = new Static1D(1);
-
-      VertexEffectMove   effect0 = new VertexEffectMove(new Static3D(0,0,+0.5f));
-      effect0.setMeshAssociation(63,-1);  // all 6 sides
-      VertexEffectRotate effect1 = new VertexEffectRotate( angle180, axisX, center );
-      effect1.setMeshAssociation(32,-1);  // back
-      VertexEffectRotate effect2 = new VertexEffectRotate( angle90 , axisX, center );
-      effect2.setMeshAssociation( 8,-1);  // bottom
-      VertexEffectRotate effect3 = new VertexEffectRotate( angle270, axisX, center );
-      effect3.setMeshAssociation( 4,-1);  // top
-      VertexEffectRotate effect4 = new VertexEffectRotate( angle270, axisY, center );
-      effect4.setMeshAssociation( 2,-1);  // left
-      VertexEffectRotate effect5 = new VertexEffectRotate( angle90 , axisY, center );
-      effect5.setMeshAssociation( 1,-1);  // right
-
-      VertexEffectDeform effect6 = new VertexEffectDeform(dVec0, dRad, dCen0, dReg);
-      VertexEffectDeform effect7 = new VertexEffectDeform(dVec1, dRad, dCen1, dReg);
-      VertexEffectDeform effect8 = new VertexEffectDeform(dVec2, dRad, dCen2, dReg);
-      VertexEffectDeform effect9 = new VertexEffectDeform(dVec3, dRad, dCen3, dReg);
-      VertexEffectDeform effect10= new VertexEffectDeform(dVec4, dRad, dCen4, dReg);
-      VertexEffectDeform effect11= new VertexEffectDeform(dVec5, dRad, dCen5, dReg);
-      VertexEffectDeform effect12= new VertexEffectDeform(dVec6, dRad, dCen6, dReg);
-      VertexEffectDeform effect13= new VertexEffectDeform(dVec7, dRad, dCen7, dReg);
-
-      mMeshes[index].apply(effect0);
-      mMeshes[index].apply(effect1);
-      mMeshes[index].apply(effect2);
-      mMeshes[index].apply(effect3);
-      mMeshes[index].apply(effect4);
-      mMeshes[index].apply(effect5);
-      mMeshes[index].apply(effect6);
-      mMeshes[index].apply(effect7);
-      mMeshes[index].apply(effect8);
-      mMeshes[index].apply(effect9);
-      mMeshes[index].apply(effect10);
-      mMeshes[index].apply(effect11);
-      mMeshes[index].apply(effect12);
-      mMeshes[index].apply(effect13);
-
-      mMeshes[index].mergeEffComponents();
-      }
-
-    return mMeshes[index].copy(true);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return getSize();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getRowChances()
-    {
-    int size = getSize();
-    float[] chances = new float[size];
-
-    for(int i=0; i<size; i++)
-      {
-      chances[i] = (i+1.0f) / size;
-      }
-
-    return chances;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public Static3D[] getRotationAxis()
-    {
-    return ROT_AXIS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getBasicAngle()
-    {
-    return 4;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeRowFromOffset(float offset)
-    {
-    return (int)(getSize()*offset);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(float offset)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
-    {
-    int numAxis = ROTATION_AXIS.length;
-
-    if( oldRotAxis == START_AXIS )
-      {
-      return rnd.nextInt(numAxis);
-      }
-    else
-      {
-      int newVector = rnd.nextInt(numAxis-1);
-      return (newVector>=oldRotAxis ? newVector+1 : newVector);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    float rowFloat = rnd.nextFloat();
-
-    for(int row=0; row<mRowChances.length; row++)
-      {
-      if( rowFloat<=mRowChances[row] ) return row;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean isSolved()
-    {
-    int index = CUBITS[0].mQuatIndex;
-
-    for(int i=1; i<NUM_CUBITS; i++)
-      {
-      if( !thereIsNoVisibleDifference(CUBITS[i], index) ) return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return if the Cubit, when rotated with its own mQuatScramble, would have looked any different
-// then if it were rotated by quaternion 'quat'.
-// No it is not so simple as the quats need to be the same - imagine a 4x4x4 cube where the two
-// middle squares get interchanged. No visible difference!
-//
-// So: this is true iff the cubit
-// a) is a corner or edge and the quaternions are the same
-// b) is inside one of the faces and after rotations by both quats it ends up on the same face.
-
-  private boolean thereIsNoVisibleDifference(Cubit cubit, int quatIndex)
-    {
-    if ( cubit.mQuatIndex == quatIndex ) return true;
-
-    int belongsToHowManyFaces = 0;
-    int size = getSize()-1;
-    float row;
-    final float MAX_ERROR = 0.01f;
-
-    for(int i=0; i<NUM_AXIS; i++)
-      {
-      row = cubit.mRotationRow[i];
-      if( (row     <MAX_ERROR && row     >-MAX_ERROR) ||
-          (row-size<MAX_ERROR && row-size>-MAX_ERROR)  ) belongsToHowManyFaces++;
-      }
-
-    switch(belongsToHowManyFaces)
-      {
-      case 0 : return true ;  // 'inside' cubit that does not lie on any face
-      case 1 :                // cubit that lies inside one of the faces
-               Static3D orig = cubit.getOrigPosition();
-               Static4D quat1 = QUATS[quatIndex];
-               Static4D quat2 = QUATS[cubit.mQuatIndex];
-
-               Static4D cubitCenter = new Static4D( orig.get0(), orig.get1(), orig.get2(), 0);
-               Static4D rotated1 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat1 );
-               Static4D rotated2 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat2 );
-
-               float row1, row2, row3, row4;
-               float ax,ay,az;
-               Static3D axis;
-               float x1 = rotated1.get0();
-               float y1 = rotated1.get1();
-               float z1 = rotated1.get2();
-               float x2 = rotated2.get0();
-               float y2 = rotated2.get1();
-               float z2 = rotated2.get2();
-
-               for(int i=0; i<NUM_AXIS; i++)
-                 {
-                 axis = ROTATION_AXIS[i];
-                 ax = axis.get0();
-                 ay = axis.get1();
-                 az = axis.get2();
-
-                 row1 = ((x1*ax + y1*ay + z1*az) - mStart) / mStep;
-                 row2 = ((x2*ax + y2*ay + z2*az) - mStart) / mStep;
-                 row3 = row1 - size;
-                 row4 = row2 - size;
-
-                 if( (row1<MAX_ERROR && row1>-MAX_ERROR && row2<MAX_ERROR && row2>-MAX_ERROR) ||
-                     (row3<MAX_ERROR && row3>-MAX_ERROR && row4<MAX_ERROR && row4>-MAX_ERROR)  )
-                   {
-                   return true;
-                   }
-                 }
-               return false;
-
-      default: return false;  // edge or corner
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// order: Up --> Right --> Front --> Down --> Left --> Back
-// (because the first implemented Solver - the two-phase Cube3 one - expects such order)
-//
-// Solved 3x3x3 Cube maps to "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"
-//
-// s : size of the cube; let index = a*s + b    (i.e. a,b = row,column)
-//
-// Up    :   index --> b<s-1 ? (s-1)*(s+4b)+a : 6*s*s -13*s +8 +a
-// Right :   index --> 6*s*s - 12*s + 7 - index
-// Front :   index --> if b==0  : s*s - 1 - index
-//                     if b==s-1: 6*s*s -11*s +6 - index
-//                     else
-//                         a==0: s*s + s-1 + 4*(b-1)*(s-1) + 2*(s-2) + s
-//                         else: s*s + s-1 + 4*(b-1)*(s-1) + 2*(s-1-a)
-// Down  :   index --> b==0 ? (s-1-a) : s*s + s-1 + 4*(b-1)*(s-1) - a
-// Left  :   index --> (s-1-a)*s + b
-// Back  :   index --> if b==s-1: s*(s-1-a)
-//                     if b==0  : 5*s*s -12*s + 8 + (s-1-a)*s
-//                     else
-//                        if a==s-1: s*s + 4*(s-2-b)*(s-1)
-//                        else     : s*s + 4*(s-2-b)*(s-1) + s + (s-2-a)*2
-
-  public String retObjectString()
-    {
-    StringBuilder objectString = new StringBuilder();
-    int size = getSize();
-    int len = size*size;
-    int cubitIndex=-1, row=-1, col=-1;
-    int color=-1, face=-1;
-
-    final int RIGHT= 0;
-    final int LEFT = 1;
-    final int UP   = 2;
-    final int DOWN = 3;
-    final int FRONT= 4;
-    final int BACK = 5;
-
-    // 'I' - interior, theoretically can happen
-    final char[] FACE_NAMES = { 'R', 'L', 'U', 'D', 'F', 'B', 'I'};
-
-    face = UP;
-
-    for(int i=0; i<len; i++)
-      {
-      row = i/size;
-      col = i%size;
-
-      cubitIndex = col<size-1 ? (size-1)*(size+4*col) + row : 6*size*size - 13*size + 8 + row;
-      color = getCubitFaceColorIndex(cubitIndex,face);
-      objectString.append(FACE_NAMES[color]);
-      }
-
-    face = RIGHT;
-
-    for(int i=0; i<len; i++)
-      {
-      cubitIndex = 6*size*size - 12*size +7 - i;
-      color = getCubitFaceColorIndex(cubitIndex,face);
-      objectString.append(FACE_NAMES[color]);
-      }
-
-     face = FRONT;
-
-    for(int i=0; i<len; i++)
-      {
-      row = i/size;
-      col = i%size;
-
-      if( col==size-1 ) cubitIndex = 6*size*size - 11*size + 6 -i;
-      else if( col==0 ) cubitIndex = size*size - 1 - i;
-      else
-        {
-        if( row==0 ) cubitIndex = size*size + size-1 + 4*(col-1)*(size-1) + 2*(size-2) + size;
-        else         cubitIndex = size*size + size-1 + 4*(col-1)*(size-1) + 2*(size-1-row);
-        }
-
-      color = getCubitFaceColorIndex(cubitIndex,face);
-      objectString.append(FACE_NAMES[color]);
-      }
-
-    face = DOWN;
-
-    for(int i=0; i<len; i++)
-      {
-      row = i/size;
-      col = i%size;
-
-      cubitIndex = col==0 ? size-1-row : size*size + size-1 + 4*(col-1)*(size-1) - row;
-      color = getCubitFaceColorIndex(cubitIndex,face);
-      objectString.append(FACE_NAMES[color]);
-      }
-
-    face = LEFT;
-
-    for(int i=0; i<len; i++)
-      {
-      row = i/size;
-      col = i%size;
-
-      cubitIndex = (size-1-row)*size + col;
-      color = getCubitFaceColorIndex(cubitIndex,face);
-      objectString.append(FACE_NAMES[color]);
-      }
-
-    face = BACK;
-
-    for(int i=0; i<len; i++)
-      {
-      row = i/size;
-      col = i%size;
-
-      if( col==size-1 ) cubitIndex = size*(size-1-row);
-      else if( col==0 ) cubitIndex = 5*size*size - 12*size + 8 + (size-1-row)*size;
-      else
-        {
-        if( row==size-1 ) cubitIndex = size*size + 4*(size-2-col)*(size-1);
-        else              cubitIndex = size*size + 4*(size-2-col)*(size-1) + size + 2*(size-2-row);
-        }
-
-      color = getCubitFaceColorIndex(cubitIndex,face);
-      objectString.append(FACE_NAMES[color]);
-      }
-
-    return objectString.toString();
-    }
-}
diff --git a/src/main/java/org/distorted/objects/RubikDiamond.java b/src/main/java/org/distorted/objects/RubikDiamond.java
deleted file mode 100644
index 76dd0516..00000000
--- a/src/main/java/org/distorted/objects/RubikDiamond.java
+++ /dev/null
@@ -1,404 +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.objects;
-
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikDiamond extends RubikObject
-{
-  private static final float SQ2 = (float)Math.sqrt(2);
-  private static final float SQ3 = (float)Math.sqrt(3);
-  private static final float SQ6 = (float)Math.sqrt(6);
-
-  private static final int FACES_PER_CUBIT =8;
-
-  // the four rotation axis of a Diamond. Must be normalized.
-  static final Static3D[] ROT_AXIS = new Static3D[]
-         {
-           new Static3D(+SQ6/3,+SQ3/3,     0),
-           new Static3D(-SQ6/3,+SQ3/3,     0),
-           new Static3D(     0,+SQ3/3,+SQ6/3),
-           new Static3D(     0,+SQ3/3,-SQ6/3)
-         };
-
-  // the eight axis that determine the faces
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(+SQ6/3,+SQ3/3,     0), new Static3D(-SQ6/3,-SQ3/3,     0),
-           new Static3D(-SQ6/3,+SQ3/3,     0), new Static3D(+SQ6/3,-SQ3/3,     0),
-           new Static3D(     0,+SQ3/3,+SQ6/3), new Static3D(     0,-SQ3/3,-SQ6/3),
-           new Static3D(     0,+SQ3/3,-SQ6/3), new Static3D(     0,-SQ3/3,+SQ6/3)
-         };
-
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_YELLOW, COLOR_WHITE,
-           COLOR_BLUE  , COLOR_GREEN,
-           COLOR_RED   , COLOR_BROWN,
-           COLOR_PINK  , COLOR_VIOLET
-         };
-
-  // All legal rotation quats of a Diamond
-  private static final Static4D[] QUATS = new Static4D[]
-         {
-           new Static4D(  0.0f,  0.0f,   0.0f,  1.0f ),
-           new Static4D(  0.0f,  1.0f,   0.0f,  0.0f ),
-           new Static4D(+SQ2/2,  0.5f,   0.0f,  0.5f ),
-           new Static4D(-SQ2/2,  0.5f,   0.0f,  0.5f ),
-           new Static4D(  0.0f,  0.5f, +SQ2/2,  0.5f ),
-           new Static4D(  0.0f,  0.5f, -SQ2/2,  0.5f ),
-           new Static4D(+SQ2/2,  0.5f,   0.0f, -0.5f ),
-           new Static4D(-SQ2/2,  0.5f,   0.0f, -0.5f ),
-           new Static4D(  0.0f,  0.5f, +SQ2/2, -0.5f ),
-           new Static4D(  0.0f,  0.5f, -SQ2/2, -0.5f ),
-           new Static4D(+SQ2/2,  0.0f, -SQ2/2,  0.0f ),
-           new Static4D(-SQ2/2,  0.0f, -SQ2/2,  0.0f )
-         };
-
-  private static final float DIST = 0.50f;
-
-  // centers of the 6 octahedrons + 8 tetrahedrons ( i.e. of the all 14 cubits)
-  private static final Static3D[] CENTERS = new Static3D[]
-         {
-           new Static3D( DIST,          0, DIST ),
-           new Static3D( DIST,          0,-DIST ),
-           new Static3D(-DIST,          0,-DIST ),
-           new Static3D(-DIST,          0, DIST ),
-           new Static3D(    0, DIST*SQ2  ,    0 ),
-           new Static3D(    0,-DIST*SQ2  ,    0 ),
-
-           new Static3D(    0, DIST*SQ2/2, DIST ),
-           new Static3D( DIST, DIST*SQ2/2,    0 ),
-           new Static3D(    0, DIST*SQ2/2,-DIST ),
-           new Static3D(-DIST, DIST*SQ2/2,    0 ),
-           new Static3D(    0,-DIST*SQ2/2, DIST ),
-           new Static3D( DIST,-DIST*SQ2/2,    0 ),
-           new Static3D(    0,-DIST*SQ2/2,-DIST ),
-           new Static3D(-DIST,-DIST*SQ2/2,    0 )
-         };
-
-  // Colors of the faces of cubits. Each cubit has 8 faces
-  private static final int[][] mFaceMap = new int[][]
-         {
-           { 6,1,8,8, 2,5,8,8 },
-           { 8,1,3,8, 8,5,7,8 },
-           { 8,8,3,4, 8,8,7,0 },
-           { 6,8,8,4, 2,8,8,0 },
-           { 6,1,3,4, 8,8,8,8 },
-           { 8,8,8,8, 2,5,7,0 },
-
-           { 6,8,8,8, 8,8,8,8 },
-           { 1,8,8,8, 8,8,8,8 },
-           { 3,8,8,8, 8,8,8,8 },
-           { 4,8,8,8, 8,8,8,8 },
-           { 2,8,8,8, 8,8,8,8 },
-           { 5,8,8,8, 8,8,8,8 },
-           { 7,8,8,8, 8,8,8,8 },
-           { 0,8,8,8, 8,8,8,8 }
-         };
-
-  private static MeshBase mOctaMesh, mTetraMesh;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikDiamond(int size, Static4D quat, DistortedTexture texture,
-               MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, 60, quat, texture, mesh, effects, moves, RubikObjectList.DIAM, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createOctaMesh()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createTetraMesh()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static4D[] getQuats()
-    {
-    return QUATS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumStickerTypes()
-    {
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getBasicStep()
-    {
-    return SQ6/6;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumCubitFaces()
-    {
-    return FACES_PER_CUBIT;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static3D[] getCubitPositions(int size)
-    {
-    return CENTERS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Static4D getQuat(int cubit)
-    {
-    switch(cubit)
-      {
-      case  0:
-      case  1:
-      case  2:
-      case  3:
-      case  4:
-      case  5:
-      case  6: return QUATS[0];                          // unit quat
-      case  7: return new Static4D( SQ2/2,0,0,SQ2/2);    //  90 along Y
-      case  8: return QUATS[1];                          // 180 along Y
-      case  9: return new Static4D(-SQ2/2,0,0,SQ2/2);    //  90 along Y
-      case 10: return new Static4D(     0,0,1,    0);    // 180 along Z
-      case 11: return new Static4D(0, SQ2/2,SQ2/2,0);    //
-      case 12: return new Static4D(     1,0,0,    0);    // 180 along X
-      case 13: return new Static4D(0,-SQ2/2,SQ2/2,0);    //
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int cubit)
-    {
-    MeshBase mesh;
-
-    if( cubit<6 )
-      {
-      if( mOctaMesh==null ) createOctaMesh();
-      mesh = mOctaMesh.copy(true);
-      }
-    else
-      {
-      if( mTetraMesh==null ) createTetraMesh();
-      mesh = mTetraMesh.copy(true);
-      }
-
-    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( getQuat(cubit), new Static3D(0,0,0) );
-    mesh.apply(quat,0xffffffff,0);
-
-    return mesh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    return mFaceMap[cubit][cubitface];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
-    {
-    float STROKE = 0.044f*side;
-    float OFF = STROKE/2 -1;
-    float OFF2 = 0.5f*side + OFF;
-    float HEIGHT = side - OFF;
-    float RADIUS = side/12.0f;
-    float ARC1_H = 0.2f*side;
-    float ARC1_W = side*0.5f;
-    float ARC2_W = 0.153f*side;
-    float ARC2_H = 0.905f*side;
-    float ARC3_W = side-ARC2_W;
-
-    float M = SQ3/2;
-    float D = (M/2 - 0.51f)*side;
-
-    paint.setAntiAlias(true);
-    paint.setStrokeWidth(STROKE);
-    paint.setColor(FACE_COLORS[face]);
-    paint.setStyle(Paint.Style.FILL);
-
-    canvas.drawRect(left,top,left+side,top+side,paint);
-
-    paint.setColor(INTERIOR_COLOR);
-    paint.setStyle(Paint.Style.STROKE);
-
-    canvas.drawLine(           left, M*HEIGHT+D,  side       +left, M*HEIGHT+D, paint);
-    canvas.drawLine(      OFF +left, M*side  +D,       OFF2  +left,          D, paint);
-    canvas.drawLine((side-OFF)+left, M*side  +D, (side-OFF2) +left,          D, paint);
-
-    canvas.drawArc( ARC1_W-RADIUS+left, M*(ARC1_H-RADIUS)+D, ARC1_W+RADIUS+left, M*(ARC1_H+RADIUS)+D, 225, 90, false, paint);
-    canvas.drawArc( ARC2_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC2_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 105, 90, false, paint);
-    canvas.drawArc( ARC3_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC3_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 345, 90, false, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return 2.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getRowChances()
-    {
-    float[] chances = new float[2];
-
-    chances[0] = 0.5f;
-    chances[1] = 1.0f;
-
-    return chances;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public Static3D[] getRotationAxis()
-    {
-    return ROT_AXIS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getBasicAngle()
-    {
-    return 3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeRowFromOffset(float offset)
-    {
-    return offset<0.25f ? 0:1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(float offset)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
-    {
-    int numAxis = ROTATION_AXIS.length;
-
-    if( oldRotAxis == START_AXIS )
-      {
-      return rnd.nextInt(numAxis);
-      }
-    else
-      {
-      int newVector = rnd.nextInt(numAxis-1);
-      return (newVector>=oldRotAxis ? newVector+1 : newVector);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    float rowFloat = rnd.nextFloat();
-
-    for(int row=0; row<mRowChances.length; row++)
-      {
-      if( rowFloat<=mRowChances[row] ) return row;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// The Diamond is solved if and only if:
-//
-// ??
-
-  public boolean isSolved()
-    {
-
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// only needed for solvers - there are no Diamond solvers ATM)
-
-  public String retObjectString()
-    {
-    return "";
-    }
-
-}
diff --git a/src/main/java/org/distorted/objects/RubikDino.java b/src/main/java/org/distorted/objects/RubikDino.java
deleted file mode 100644
index 4f927453..00000000
--- a/src/main/java/org/distorted/objects/RubikDino.java
+++ /dev/null
@@ -1,463 +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.objects;
-
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.effect.VertexEffectDeform;
-import org.distorted.library.effect.VertexEffectMove;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.effect.VertexEffectScale;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshPolygon;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.RubikSurfaceView;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class RubikDino extends RubikObject
-{
-  private static final float SQ2 = (float)Math.sqrt(2);
-  private static final float SQ3 = (float)Math.sqrt(3);
-
-  // the four rotation axis of a RubikDino. Must be normalized.
-  static final Static3D[] ROT_AXIS = new Static3D[]
-         {
-           new Static3D(+SQ3/3,+SQ3/3,+SQ3/3),
-           new Static3D(+SQ3/3,+SQ3/3,-SQ3/3),
-           new Static3D(+SQ3/3,-SQ3/3,+SQ3/3),
-           new Static3D(+SQ3/3,-SQ3/3,-SQ3/3)
-         };
-
-  // the six axis that determine the faces
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(1,0,0), new Static3D(-1,0,0),
-           new Static3D(0,1,0), new Static3D(0,-1,0),
-           new Static3D(0,0,1), new Static3D(0,0,-1)
-         };
-
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_YELLOW, COLOR_WHITE,
-           COLOR_BLUE  , COLOR_GREEN,
-           COLOR_RED   , COLOR_BROWN
-         };
-
-  // All legal rotation quats of a RubikDino
-  static final Static4D[] QUATS = new Static4D[]
-         {
-           new Static4D(  0.0f,  0.0f,  0.0f,  1.0f ),
-           new Static4D(  0.5f,  0.5f,  0.5f, -0.5f ),
-           new Static4D(  0.0f,  0.0f,  1.0f,  0.0f ),
-           new Static4D(  0.5f, -0.5f, -0.5f, -0.5f ),
-           new Static4D(  0.5f,  0.5f,  0.5f,  0.5f ),
-           new Static4D(  0.5f,  0.5f, -0.5f, -0.5f ),
-           new Static4D(  0.5f, -0.5f,  0.5f, -0.5f ),
-           new Static4D(  0.5f, -0.5f, -0.5f,  0.5f ),
-           new Static4D(  0.0f,  1.0f,  0.0f,  0.0f ),
-           new Static4D(  0.5f, -0.5f,  0.5f,  0.5f ),
-           new Static4D(  1.0f,  0.0f,  0.0f,  0.0f ),
-           new Static4D(  0.5f,  0.5f, -0.5f,  0.5f )
-         };
-
-  // centers of the 12 edges. Must be in the same order like QUATs above.
-  private static final Static3D[] CENTERS = new Static3D[]
-         {
-           new Static3D( 0.0f, 1.5f, 1.5f ),
-           new Static3D( 1.5f, 0.0f, 1.5f ),
-           new Static3D( 0.0f,-1.5f, 1.5f ),
-           new Static3D(-1.5f, 0.0f, 1.5f ),
-           new Static3D( 1.5f, 1.5f, 0.0f ),
-           new Static3D( 1.5f,-1.5f, 0.0f ),
-           new Static3D(-1.5f,-1.5f, 0.0f ),
-           new Static3D(-1.5f, 1.5f, 0.0f ),
-           new Static3D( 0.0f, 1.5f,-1.5f ),
-           new Static3D( 1.5f, 0.0f,-1.5f ),
-           new Static3D( 0.0f,-1.5f,-1.5f ),
-           new Static3D(-1.5f, 0.0f,-1.5f )
-         };
-
-  private static MeshBase mMesh;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikDino(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-            DistortedEffects effects, int[][] moves, RubikObjectList obj, Resources res, int scrWidth)
-    {
-    super(size, 60, quat, texture, mesh, effects, moves, obj, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createBasicMesh()
-    {
-    final float ANGLE = (float)((180/Math.PI)*(Math.atan(SQ2)));
-
-    final int MESHES=4;
-
-    float D = 0.02f;
-    float E = 0.5f*SQ2;
-    float F = 0.5f;
-
-    float[] bands0 = { 1.0f    , 0,
-                       1.0f-2*D, D*0.25f,
-                       1.0f-4*D, D*0.35f,
-                       1.0f-8*D, D*0.6f,
-                       0.60f   , D*1.0f,
-                       0.30f   , D*1.375f,
-                       0.0f    , D*1.4f };
-
-    float[] vertices0 = { -F,F/3, 0,-2*F/3, +F,F/3 };
-
-    MeshBase[] meshes = new MeshPolygon[MESHES];
-    meshes[0] = new MeshPolygon(vertices0, bands0, 2, 5);
-    meshes[0].setEffectAssociation(0,1,0);
-    meshes[1] = meshes[0].copy(true);
-    meshes[1].setEffectAssociation(0,2,0);
-
-    float[] bands1 = { 1.0f    , 0,
-                       0.50f   , 0.10f,
-                       0.0f    , 0.20f };
-
-    float[] vertices1 = { -E/2,-E*(SQ3/6), E/2,-E*(SQ3/6), 0,E*(SQ3/3) };
-
-    meshes[2] = new MeshPolygon(vertices1, bands1, 1, 2);
-    meshes[2].setEffectAssociation(0,4,0);
-    meshes[3] = meshes[2].copy(true);
-    meshes[3].setEffectAssociation(0,8,0);
-
-    mMesh = new MeshJoined(meshes);
-
-    Static3D a0 = new Static3D(     0,-3*F,    0 );
-    Static3D a1 = new Static3D(     0,   0, -3*F );
-    Static3D a2 = new Static3D(  -3*F,   0,    0 );
-    Static3D a3 = new Static3D(  +3*F,   0,    0 );
-
-    Static3D v0 = new Static3D(     0,-3*F/2, 3*F/2 );
-    Static3D v1 = new Static3D(     0, 3*F/2,-3*F/2 );
-    Static3D v2 = new Static3D(  -3*F, 3*F/2, 3*F/2 );
-    Static3D v3 = new Static3D(  +3*F, 3*F/2, 3*F/2 );
-
-    float d1 = 1.0f;
-    float d2 =-0.10f;
-    float d3 =-0.10f;
-    float d4 = 0.40f;
-
-    Static3D dCen0 = new Static3D( d1*a0.get0(), d1*a0.get1(), d1*a0.get2() );
-    Static3D dCen1 = new Static3D( d1*a1.get0(), d1*a1.get1(), d1*a1.get2() );
-    Static3D dCen2 = new Static3D( d1*a2.get0(), d1*a2.get1(), d1*a2.get2() );
-    Static3D dCen3 = new Static3D( d1*a3.get0(), d1*a3.get1(), d1*a3.get2() );
-
-    Static3D dVec0 = new Static3D( d3*v0.get0(), d3*v0.get1(), d3*v0.get2() );
-    Static3D dVec1 = new Static3D( d3*v1.get0(), d3*v1.get1(), d3*v1.get2() );
-    Static3D dVec2 = new Static3D( d2*v2.get0(), d2*v2.get1(), d2*v2.get2() );
-    Static3D dVec3 = new Static3D( d2*v3.get0(), d2*v3.get1(), d2*v3.get2() );
-
-    Static4D dReg  = new Static4D(0,0,0,d4);
-    Static1D dRad  = new Static1D(1);
-
-    Static1D angle1 = new Static1D(+ANGLE);
-    Static1D angle2 = new Static1D(-ANGLE);
-
-    Static3D axisX  = new Static3D(1,0,0);
-    Static3D axisY  = new Static3D(0,1,0);
-    Static3D axisZ  = new Static3D(0,-1,1);
-
-    Static3D center0= new Static3D(0,0,0);
-    Static3D center1= new Static3D(0,-3*F,0);
-
-    VertexEffectScale   effect0 = new VertexEffectScale ( new Static3D(3,3,3) );
-    VertexEffectMove    effect1 = new VertexEffectMove  ( new Static3D(0,-F,0) );
-    VertexEffectRotate  effect2 = new VertexEffectRotate( new Static1D(90), axisX, center0 );
-    VertexEffectScale   effect3 = new VertexEffectScale ( new Static3D(1,-1,1) );
-    VertexEffectMove    effect4 = new VertexEffectMove  ( new Static3D(3*E/2,E*(SQ3/2)-3*F,0) );
-    VertexEffectRotate  effect5 = new VertexEffectRotate( new Static1D(+90), axisY, center1 );
-    VertexEffectScale   effect6 = new VertexEffectScale ( new Static3D(-1,1,1) );
-    VertexEffectRotate  effect7 = new VertexEffectRotate( new Static1D( 45), axisX, center1 );
-    VertexEffectRotate  effect8 = new VertexEffectRotate( angle1           , axisZ, center1 );
-    VertexEffectRotate  effect9 = new VertexEffectRotate( angle2           , axisZ, center1 );
-
-    VertexEffectDeform  effect10= new VertexEffectDeform(dVec0, dRad, dCen0, dReg);
-    VertexEffectDeform  effect11= new VertexEffectDeform(dVec1, dRad, dCen1, dReg);
-    VertexEffectDeform  effect12= new VertexEffectDeform(dVec2, dRad, dCen2, dReg);
-    VertexEffectDeform  effect13= new VertexEffectDeform(dVec3, dRad, dCen3, dReg);
-
-    effect0.setMeshAssociation(15,-1);  // apply to meshes 0,1,2,3
-    effect1.setMeshAssociation( 3,-1);  // apply to meshes 0,1
-    effect2.setMeshAssociation( 2,-1);  // apply to mesh 1
-    effect3.setMeshAssociation( 2,-1);  // apply to mesh 0
-    effect4.setMeshAssociation(12,-1);  // apply to meshes 2,3
-    effect5.setMeshAssociation(12,-1);  // apply to meshes 2,3
-    effect6.setMeshAssociation( 8,-1);  // apply to mesh 3
-    effect7.setMeshAssociation(12,-1);  // apply to meshes 2,3
-    effect8.setMeshAssociation( 4,-1);  // apply to mesh 2
-    effect9.setMeshAssociation( 8,-1);  // apply to mesh 3
-    effect10.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
-    effect11.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
-    effect12.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
-    effect13.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
-
-    mMesh.apply(effect0);
-    mMesh.apply(effect1);
-    mMesh.apply(effect2);
-    mMesh.apply(effect3);
-    mMesh.apply(effect4);
-    mMesh.apply(effect5);
-    mMesh.apply(effect6);
-    mMesh.apply(effect7);
-    mMesh.apply(effect8);
-    mMesh.apply(effect9);
-    mMesh.apply(effect10);
-    mMesh.apply(effect11);
-    mMesh.apply(effect12);
-    mMesh.apply(effect13);
-
-    mMesh.mergeEffComponents();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int mulQuat(int q1, int q2)
-    {
-    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
-
-    float rX = result.get0();
-    float rY = result.get1();
-    float rZ = result.get2();
-    float rW = result.get3();
-
-    final float MAX_ERROR = 0.1f;
-    float dX,dY,dZ,dW;
-
-    for(int i=0; i<QUATS.length; i++)
-      {
-      dX = QUATS[i].get0() - rX;
-      dY = QUATS[i].get1() - rY;
-      dZ = QUATS[i].get2() - rZ;
-      dW = QUATS[i].get3() - rW;
-
-      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
-          dY<MAX_ERROR && dY>-MAX_ERROR &&
-          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
-          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
-
-      dX = QUATS[i].get0() + rX;
-      dY = QUATS[i].get1() + rY;
-      dZ = QUATS[i].get2() + rZ;
-      dW = QUATS[i].get3() + rW;
-
-      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
-          dY<MAX_ERROR && dY>-MAX_ERROR &&
-          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
-          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.5f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static4D[] getQuats()
-    {
-    return QUATS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getBasicStep()
-    {
-    return SQ3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumStickerTypes()
-    {
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumCubitFaces()
-    {
-    return 4;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static3D[] getCubitPositions(int size)
-    {
-    return CENTERS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int cubit)
-    {
-    if( mMesh==null ) createBasicMesh();
-
-    MeshBase mesh = mMesh.copy(true);
-    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( QUATS[cubit], new Static3D(0,0,0) );
-    mesh.apply(quat,0xffffffff,0);
-
-    return mesh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
-    {
-    float STROKE = 0.04f*side;
-    float L= left;
-    float H= 0.333f*side;
-    float LEN = 0.5f*side;
-
-    paint.setAntiAlias(true);
-    paint.setStrokeWidth(STROKE);
-    paint.setColor(FACE_COLORS[face]);
-    paint.setStyle(Paint.Style.FILL);
-
-    canvas.drawRect(left,top,left+side,top+side,paint);
-
-    paint.setColor(INTERIOR_COLOR);
-    paint.setStyle(Paint.Style.STROKE);
-
-    canvas.drawLine( L      , H,  L+2*LEN, H    , paint);
-    canvas.drawLine( L      , H,  L+  LEN, H+LEN, paint);
-    canvas.drawLine( L+2*LEN, H,  L+  LEN, H+LEN, paint);
-
-    float S1 = 0.150f*side;
-    float S2 = 0.090f*side;
-    float X  = 0.7f*S2;
-    float Y  = 0.2f*S1;
-
-    float LA = left+0.500f*side;
-    float RA = left;
-    float TA = 0.333f*side;
-    float BA = 0.833f*side;
-
-    canvas.drawArc( RA+X        , TA     , RA+X+S2  , TA+S2, 135,135, false, paint);
-    canvas.drawArc( RA+side-S2-X, TA     , RA+side-X, TA+S2, 270,135, false, paint);
-    canvas.drawArc( LA-S1/2     , BA-S1-Y, LA+S1/2  , BA-Y ,  45, 90, false, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return 2.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getRowChances()
-    {
-    float[] chances = new float[3];
-
-    chances[0] = 0.5f;
-    chances[1] = 0.5f;
-    chances[2] = 1.0f;
-
-    return chances;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public Static3D[] getRotationAxis()
-    {
-    return ROT_AXIS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getBasicAngle()
-    {
-    return 3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeRowFromOffset(float offset)
-    {
-    return offset<0.5f ? 0:2;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(float offset)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
-    {
-    int numAxis = ROTATION_AXIS.length;
-
-    if( oldRotAxis == START_AXIS )
-      {
-      return rnd.nextInt(numAxis);
-      }
-    else
-      {
-      int newVector = rnd.nextInt(numAxis-1);
-      return (newVector>=oldRotAxis ? newVector+1 : newVector);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// only needed for solvers - there are no Dino solvers ATM)
-
-  public String retObjectString()
-    {
-    return "";
-    }
-
-}
diff --git a/src/main/java/org/distorted/objects/RubikDino4.java b/src/main/java/org/distorted/objects/RubikDino4.java
deleted file mode 100644
index 2546ba36..00000000
--- a/src/main/java/org/distorted/objects/RubikDino4.java
+++ /dev/null
@@ -1,111 +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.objects;
-
-import android.content.res.Resources;
-
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikDino4 extends RubikDino
-{
-  private static final int[] mFaceMap = {4,4, 2,2, 2,2, 4,4,
-                                         0,0, 2,2, 1,1, 4,4,
-                                         0,0, 0,0, 1,1, 1,1 };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikDino4(int size, Static4D quat, DistortedTexture texture,
-             MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, quat, texture, mesh, effects, moves, RubikObjectList.DIN4, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    switch(cubitface)
-      {
-      case 0 : return mFaceMap[2*cubit];
-      case 1 : return mFaceMap[2*cubit+1];
-      default: return NUM_FACES;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    return (oldRotAxis==START_AXIS) ? ((newRotAxis==1 || newRotAxis==2) ? 0:2) : (oldRotAxis+newRotAxis==3 ? 2-oldRow : oldRow);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Dino4 is solved if and only if the four groups of three same-colored cubits are each 'together'
-// (actually we need to check only 3 first groups - if those are correct, the fourth one also needs
-// to be correct).
-//
-// White group : (X,Y,Z) = 10,11,6
-// Red group   : (X,Y,Z) = 0,3,7
-// Blue group  : (X,Y,Z) = 2,1,5
-// Yellow group: (X,Y,Z) = 8,9,4
-//
-// A group of 3 cubits is 'together' if and only if they are all rotated with one quat - but we cannot
-// forget that the whole Dino can be mirrored! (so then qY = qX*Q2 and qZ = qX*Q8 )
-//
-// X cubits: 0, 2, 8, 10
-// Y cubits: 1, 3, 9, 11
-// Z cubits: 4, 5, 6, 7
-
-  public boolean isSolved()
-    {
-    int redX = CUBITS[0].mQuatIndex;
-    int bluX = CUBITS[2].mQuatIndex;
-    int yelX = CUBITS[8].mQuatIndex;
-
-    if (CUBITS[3].mQuatIndex == redX && CUBITS[7].mQuatIndex == redX &&
-        CUBITS[1].mQuatIndex == bluX && CUBITS[5].mQuatIndex == bluX &&
-        CUBITS[9].mQuatIndex == yelX && CUBITS[4].mQuatIndex == yelX  ) return true;
-
-    if (CUBITS[3].mQuatIndex != mulQuat(redX,2)) return false;
-    if (CUBITS[7].mQuatIndex != mulQuat(redX,8)) return false;
-    if (CUBITS[1].mQuatIndex != mulQuat(bluX,2)) return false;
-    if (CUBITS[5].mQuatIndex != mulQuat(bluX,8)) return false;
-    if (CUBITS[9].mQuatIndex != mulQuat(yelX,2)) return false;
-    if (CUBITS[4].mQuatIndex != mulQuat(yelX,8)) return false;
-
-    return true;
-    }
-}
diff --git a/src/main/java/org/distorted/objects/RubikDino6.java b/src/main/java/org/distorted/objects/RubikDino6.java
deleted file mode 100644
index 9f3040b2..00000000
--- a/src/main/java/org/distorted/objects/RubikDino6.java
+++ /dev/null
@@ -1,109 +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.objects;
-
-import android.content.res.Resources;
-
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.RubikSurfaceView;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikDino6 extends RubikDino
-{
-  private static final int[] mFaceMap = {4,2, 0,4, 4,3, 1,4,
-                                         2,0, 3,0, 3,1, 2,1,
-                                         5,2, 0,5, 5,3, 1,5 };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikDino6(int size, Static4D quat, DistortedTexture texture,
-             MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, quat, texture, mesh, effects, moves, RubikObjectList.DINO, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    switch(cubitface)
-      {
-      case 0 : return mFaceMap[2*cubit];
-      case 1 : return mFaceMap[2*cubit+1];
-      default: return NUM_FACES;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    return (oldRotAxis==START_AXIS) ? (rnd.nextFloat()<=0.5f ? 0:2) : (oldRotAxis+newRotAxis==3 ? 2-oldRow : oldRow);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Dino6 is solved if and only if:
-//
-// All four 'X' cubits (i.e. those whose longest edge goes along the X axis) are rotated
-// by the same quaternion qX, similarly all four 'Y' cubits by the same qY and all four 'Z'
-// by the same qZ, and then either:
-//
-// a) qX = qY = qZ
-// b) qY = qX*Q2 and qZ = qX*Q8  (i.e. swap of WHITE and YELLOW faces)
-// c) qX = qY*Q2 and qZ = qY*Q10 (i.e. swap of BLUE and GREEN faces)
-// d) qX = qZ*Q8 and qY = qZ*Q10 (i.e. swap of RED and BROWN faces)
-//
-// BUT: cases b), c) and d) are really the same - it's all just a mirror image of the original.
-//
-// X cubits: 0, 2, 8, 10
-// Y cubits: 1, 3, 9, 11
-// Z cubits: 4, 5, 6, 7
-
-  public boolean isSolved()
-    {
-    int qX = CUBITS[0].mQuatIndex;
-    int qY = CUBITS[1].mQuatIndex;
-    int qZ = CUBITS[4].mQuatIndex;
-
-    if( CUBITS[2].mQuatIndex != qX || CUBITS[8].mQuatIndex != qX || CUBITS[10].mQuatIndex != qX ||
-        CUBITS[3].mQuatIndex != qY || CUBITS[9].mQuatIndex != qY || CUBITS[11].mQuatIndex != qY ||
-        CUBITS[5].mQuatIndex != qZ || CUBITS[6].mQuatIndex != qZ || CUBITS[ 7].mQuatIndex != qZ  )
-      {
-      return false;
-      }
-
-    return ( qX==qY && qX==qZ ) || ( qY==mulQuat(qX,2) && qZ==mulQuat(qX,8) );
-    }
-}
diff --git a/src/main/java/org/distorted/objects/RubikHelicopter.java b/src/main/java/org/distorted/objects/RubikHelicopter.java
deleted file mode 100644
index 704a2815..00000000
--- a/src/main/java/org/distorted/objects/RubikHelicopter.java
+++ /dev/null
@@ -1,795 +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.objects;
-
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.effect.VertexEffectDeform;
-import org.distorted.library.effect.VertexEffectMove;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.effect.VertexEffectScale;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshPolygon;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.mesh.MeshTriangle;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.RubikSurfaceView;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikHelicopter extends RubikObject
-{
-  private static final float SQ2 = (float)Math.sqrt(2);
-  private static final float SQ3 = (float)Math.sqrt(3);
-
-  private static final int FACES_PER_CUBIT =6;
-
-  // the six rotation axis of a Helicopter. Must be normalized.
-  static final Static3D[] ROT_AXIS = new Static3D[]
-         {
-           new Static3D(     0, +SQ2/2, -SQ2/2),
-           new Static3D(     0, -SQ2/2, -SQ2/2),
-           new Static3D(+SQ2/2,      0, -SQ2/2),
-           new Static3D(-SQ2/2,      0, -SQ2/2),
-           new Static3D(+SQ2/2, -SQ2/2,      0),
-           new Static3D(-SQ2/2, -SQ2/2,      0)
-         };
-
-  // the six axis that determine the faces
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(1,0,0), new Static3D(-1,0,0),
-           new Static3D(0,1,0), new Static3D(0,-1,0),
-           new Static3D(0,0,1), new Static3D(0,0,-1)
-         };
-
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_YELLOW, COLOR_WHITE,
-           COLOR_BLUE  , COLOR_GREEN,
-           COLOR_RED   , COLOR_BROWN
-         };
-
-  // All legal rotation quats of a HELICOPTER (same as the Cube!)
-  private static final Static4D[] QUATS = new Static4D[]
-         {
-           new Static4D( 0.00f,  0.00f,  0.00f,  1.00f ),
-           new Static4D( 1.00f,  0.00f,  0.00f,  0.00f ),
-           new Static4D( 0.00f,  1.00f,  0.00f,  0.00f ),
-           new Static4D( 0.00f,  0.00f,  1.00f,  0.00f ),
-
-           new Static4D( SQ2/2,  SQ2/2,  0.00f,  0.00f ),
-           new Static4D( SQ2/2, -SQ2/2,  0.00f,  0.00f ),
-           new Static4D( SQ2/2,  0.00f,  SQ2/2,  0.00f ),
-           new Static4D( SQ2/2,  0.00f, -SQ2/2,  0.00f ),
-           new Static4D( SQ2/2,  0.00f,  0.00f,  SQ2/2 ),
-           new Static4D( SQ2/2,  0.00f,  0.00f, -SQ2/2 ),
-           new Static4D( 0.00f,  SQ2/2,  SQ2/2,  0.00f ),
-           new Static4D( 0.00f,  SQ2/2, -SQ2/2,  0.00f ),
-           new Static4D( 0.00f,  SQ2/2,  0.00f,  SQ2/2 ),
-           new Static4D( 0.00f,  SQ2/2,  0.00f, -SQ2/2 ),
-           new Static4D( 0.00f,  0.00f,  SQ2/2,  SQ2/2 ),
-           new Static4D( 0.00f,  0.00f,  SQ2/2, -SQ2/2 ),
-
-           new Static4D( 0.50f,  0.50f,  0.50f,  0.50f ),
-           new Static4D( 0.50f,  0.50f,  0.50f, -0.50f ),
-           new Static4D( 0.50f,  0.50f, -0.50f,  0.50f ),
-           new Static4D( 0.50f,  0.50f, -0.50f, -0.50f ),
-           new Static4D( 0.50f, -0.50f,  0.50f,  0.50f ),
-           new Static4D( 0.50f, -0.50f,  0.50f, -0.50f ),
-           new Static4D( 0.50f, -0.50f, -0.50f,  0.50f ),
-           new Static4D( 0.50f, -0.50f, -0.50f, -0.50f )
-         };
-
-  private static final float DIST_CORNER = 0.50f;
-  private static final float DIST_CENTER = 0.50f;
-  private static final float XY_CENTER   = DIST_CORNER/3;
-
-  // centers of the 8 corners + 6*4 face triangles ( i.e. of the all 32 cubits)
-  private static final Static3D[] CENTERS = new Static3D[]
-         {
-           new Static3D(   DIST_CORNER,   DIST_CORNER,   DIST_CORNER ),
-           new Static3D(   DIST_CORNER,   DIST_CORNER,  -DIST_CORNER ),
-           new Static3D(   DIST_CORNER,  -DIST_CORNER,   DIST_CORNER ),
-           new Static3D(   DIST_CORNER,  -DIST_CORNER,  -DIST_CORNER ),
-           new Static3D(  -DIST_CORNER,   DIST_CORNER,   DIST_CORNER ),
-           new Static3D(  -DIST_CORNER,   DIST_CORNER,  -DIST_CORNER ),
-           new Static3D(  -DIST_CORNER,  -DIST_CORNER,   DIST_CORNER ),
-           new Static3D(  -DIST_CORNER,  -DIST_CORNER,  -DIST_CORNER ),
-
-           new Static3D(   DIST_CENTER,     XY_CENTER,     XY_CENTER ),
-           new Static3D(   DIST_CENTER,     XY_CENTER,    -XY_CENTER ),
-           new Static3D(   DIST_CENTER,    -XY_CENTER,     XY_CENTER ),
-           new Static3D(   DIST_CENTER,    -XY_CENTER,    -XY_CENTER ),
-
-           new Static3D(  -DIST_CENTER,     XY_CENTER,     XY_CENTER ),
-           new Static3D(  -DIST_CENTER,     XY_CENTER,    -XY_CENTER ),
-           new Static3D(  -DIST_CENTER,    -XY_CENTER,     XY_CENTER ),
-           new Static3D(  -DIST_CENTER,    -XY_CENTER,    -XY_CENTER ),
-
-           new Static3D(   XY_CENTER  ,   DIST_CENTER,     XY_CENTER ),
-           new Static3D(   XY_CENTER  ,   DIST_CENTER,    -XY_CENTER ),
-           new Static3D(  -XY_CENTER  ,   DIST_CENTER,     XY_CENTER ),
-           new Static3D(  -XY_CENTER  ,   DIST_CENTER,    -XY_CENTER ),
-
-           new Static3D(   XY_CENTER  ,  -DIST_CENTER,     XY_CENTER ),
-           new Static3D(   XY_CENTER  ,  -DIST_CENTER,    -XY_CENTER ),
-           new Static3D(  -XY_CENTER  ,  -DIST_CENTER,     XY_CENTER ),
-           new Static3D(  -XY_CENTER  ,  -DIST_CENTER,    -XY_CENTER ),
-
-           new Static3D(   XY_CENTER  ,     XY_CENTER,   DIST_CENTER ),
-           new Static3D(   XY_CENTER  ,    -XY_CENTER,   DIST_CENTER ),
-           new Static3D(  -XY_CENTER  ,     XY_CENTER,   DIST_CENTER ),
-           new Static3D(  -XY_CENTER  ,    -XY_CENTER,   DIST_CENTER ),
-
-           new Static3D(   XY_CENTER  ,     XY_CENTER,  -DIST_CENTER ),
-           new Static3D(   XY_CENTER  ,    -XY_CENTER,  -DIST_CENTER ),
-           new Static3D(  -XY_CENTER  ,     XY_CENTER,  -DIST_CENTER ),
-           new Static3D(  -XY_CENTER  ,    -XY_CENTER,  -DIST_CENTER ),
-         };
-
-  // Colors of the faces of cubits. Each cubit has 6 faces
-  private static final int[][] mFaceMap = new int[][]
-         {
-           { 4,2,0, 6,6,6 },
-           { 0,2,5, 6,6,6 },
-           { 4,0,3, 6,6,6 },
-           { 5,3,0, 6,6,6 },
-           { 1,2,4, 6,6,6 },
-           { 5,2,1, 6,6,6 },
-           { 4,3,1, 6,6,6 },
-           { 1,3,5, 6,6,6 },
-
-           { 0 , 6,6,6,6,6 },
-           { 0 , 6,6,6,6,6 },
-           { 0 , 6,6,6,6,6 },
-           { 0 , 6,6,6,6,6 },
-
-           { 1 , 6,6,6,6,6 },
-           { 1 , 6,6,6,6,6 },
-           { 1 , 6,6,6,6,6 },
-           { 1 , 6,6,6,6,6 },
-
-           { 2 , 6,6,6,6,6 },
-           { 2 , 6,6,6,6,6 },
-           { 2 , 6,6,6,6,6 },
-           { 2 , 6,6,6,6,6 },
-
-           { 3 , 6,6,6,6,6 },
-           { 3 , 6,6,6,6,6 },
-           { 3 , 6,6,6,6,6 },
-           { 3 , 6,6,6,6,6 },
-
-           { 4 , 6,6,6,6,6 },
-           { 4 , 6,6,6,6,6 },
-           { 4 , 6,6,6,6,6 },
-           { 4 , 6,6,6,6,6 },
-
-           { 5 , 6,6,6,6,6 },
-           { 5 , 6,6,6,6,6 },
-           { 5 , 6,6,6,6,6 },
-           { 5 , 6,6,6,6,6 },
-         };
-
-  private static int[] QUAT_INDICES =
-      { 0,13,14,1,12,2,3,7,20,6,13,17,7,23,18,12,22,10,8,16,11,21,19,9,3,15,14,0,5,2,1,4 };
-
-  private static MeshBase mCornerMesh, mFaceMesh;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikHelicopter(int size, Static4D quat, DistortedTexture texture,
-                  MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, 60, quat, texture, mesh, effects, moves, RubikObjectList.HELI, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createCornerMesh()
-    {
-    float D = 0.02f;
-    float E = 0.5f;
-    float F = SQ2/4;
-
-    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
-
-    float[] bands0 = { 1.0f    , 0,
-                       1.0f-2*D, D*0.25f,
-                       1.0f-4*D, D*0.35f,
-                       1.0f-8*D, D*0.6f,
-                       0.60f   , D*1.0f,
-                       0.30f   , D*1.375f,
-                       0.0f    , D*1.4f };
-
-    MeshBase[] meshes = new MeshBase[6];
-
-    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
-    meshes[0].setEffectAssociation(0,1,0);
-    meshes[1] = meshes[0].copy(true);
-    meshes[1].setEffectAssociation(0,2,0);
-    meshes[2] = meshes[0].copy(true);
-    meshes[2].setEffectAssociation(0,4,0);
-
-    float[] vertices1 = { -F,-1.0f/12, +F,-1.0f/12, 0,1.0f/6 };
-    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
-
-    meshes[3] = new MeshPolygon(vertices1,bands1,1,5);
-    meshes[3].setEffectAssociation(0,8,0);
-    meshes[4] = meshes[3].copy(true);
-    meshes[4].setEffectAssociation(0,16,0);
-    meshes[5] = meshes[3].copy(true);
-    meshes[5].setEffectAssociation(0,32,0);
-
-    mCornerMesh = new MeshJoined(meshes);
-
-    Static3D axisX  = new Static3D(1,0,0);
-    Static3D axisY  = new Static3D(0,1,0);
-    Static3D axis0  = new Static3D(-SQ2/2,0,SQ2/2);
-    Static3D axis1  = new Static3D(+SQ3/3,+SQ3/3,+SQ3/3);
-    Static1D angle1 = new Static1D(+90);
-    Static1D angle2 = new Static1D(-90);
-    Static1D angle3 = new Static1D(-135);
-    Static1D angle4 = new Static1D(90);
-    Static1D angle5 = new Static1D(120);
-    Static1D angle6 = new Static1D(240);
-    Static3D center1= new Static3D(0,0,0);
-    Static3D center2= new Static3D(-0.25f,-0.25f,-0.25f);
-    Static3D move1  = new Static3D(-E/4,-E/4,0);
-    Static3D move2  = new Static3D(-0.25f,(-1.0f/6)-0.25f,-0.25f);
-
-    float d0 =-0.04f;
-    float d1 = 0.04f;
-    float r0 = 0.15f;
-    float r1 = 0.10f;
-
-    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
-    Static3D vec1   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(-SQ3/3));
-    Static3D vec2   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(-SQ3/3));
-    Static3D vec3   = new Static3D(d1*(-SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
-
-    Static1D radius = new Static1D(0.5f);
-
-    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
-    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
-    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
-    Static3D cent3  = new Static3D( 0.0f, 0.0f,-0.5f);
-
-    Static4D reg0   = new Static4D(0,0,0,r0);
-    Static4D reg1   = new Static4D(0,0,0,r1);
-
-    VertexEffectMove   effect0 = new VertexEffectMove(move1);
-    VertexEffectScale  effect1 = new VertexEffectScale(new Static3D(1,1,-1));
-    VertexEffectRotate effect2 = new VertexEffectRotate(angle1,axisX,center1);
-    VertexEffectRotate effect3 = new VertexEffectRotate(angle2,axisY,center1);
-    VertexEffectMove   effect4 = new VertexEffectMove(move2);
-    VertexEffectRotate effect5 = new VertexEffectRotate(angle1,axisX,center2);
-    VertexEffectRotate effect6 = new VertexEffectRotate(angle3,axisY,center2);
-    VertexEffectRotate effect7 = new VertexEffectRotate(angle4,axis0,center2);
-    VertexEffectRotate effect8 = new VertexEffectRotate(angle5,axis1,center2);
-    VertexEffectRotate effect9 = new VertexEffectRotate(angle6,axis1,center2);
-
-    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
-    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
-    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
-    VertexEffectDeform effect13= new VertexEffectDeform(vec3,radius,cent3,reg1);
-
-    effect0.setMeshAssociation( 7,-1);  // meshes 0,1,2
-    effect1.setMeshAssociation( 6,-1);  // meshes 1,2
-    effect2.setMeshAssociation( 2,-1);  // mesh 1
-    effect3.setMeshAssociation( 4,-1);  // mesh 2
-    effect4.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect5.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect6.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect7.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect8.setMeshAssociation(16,-1);  // mesh 4
-    effect9.setMeshAssociation(32,-1);  // mesh 5
-
-    effect10.setMeshAssociation(63,-1); // all meshes
-    effect11.setMeshAssociation(63,-1); // all meshes
-    effect12.setMeshAssociation(63,-1); // all meshes
-    effect13.setMeshAssociation(63,-1); // all meshes
-
-    mCornerMesh.apply(effect0);
-    mCornerMesh.apply(effect1);
-    mCornerMesh.apply(effect2);
-    mCornerMesh.apply(effect3);
-    mCornerMesh.apply(effect4);
-    mCornerMesh.apply(effect5);
-    mCornerMesh.apply(effect6);
-    mCornerMesh.apply(effect7);
-    mCornerMesh.apply(effect8);
-    mCornerMesh.apply(effect9);
-
-    mCornerMesh.apply(effect10);
-    mCornerMesh.apply(effect11);
-    mCornerMesh.apply(effect12);
-    mCornerMesh.apply(effect13);
-
-    mCornerMesh.mergeEffComponents();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createFaceMesh()
-    {
-    MeshBase[] meshes = new MeshBase[6];
-
-    float D = 0.02f;
-    float E = 0.5f;
-    float F = SQ2/4;
-    float G = 1.0f/12;
-
-    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
-
-    float[] bands0 = { 1.0f    , 0,
-                       1.0f-2*D, D*0.25f,
-                       1.0f-4*D, D*0.35f,
-                       1.0f-8*D, D*0.6f,
-                       0.60f   , D*1.0f,
-                       0.30f   , D*1.375f,
-                       0.0f    , D*1.4f };
-
-    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
-    meshes[0].setEffectAssociation(0,1,0);
-
-    float[] vertices1 = { -F,-G, +F,-G, 0,2*G};
-
-    float[] bands1 = { 1.0f   , 0.0f,
-                       0.5f   , 0.01f,
-                       0.0f   , 0.01f };
-
-    meshes[1] = new MeshPolygon(vertices1, bands1, 1, 3);
-    meshes[1].setEffectAssociation(0,2,0);
-
-    float[] vertices2 = { -E/2,-F/3, +E/2,-F/3, 0,2*F/3};
-
-    float[] bands2 = { 1.0f   , 0.0f,
-                       0.5f   , 0.01f,
-                       0.0f   , 0.01f };
-
-    meshes[2] = new MeshPolygon(vertices2, bands2, 1, 3);
-    meshes[2].setEffectAssociation(0,4,0);
-    meshes[3] = meshes[2].copy(true);
-    meshes[3].setEffectAssociation(0,8,0);
-    meshes[4] = new MeshTriangle(1);
-    meshes[4].setEffectAssociation(0,16,0);
-    meshes[5] = new MeshTriangle(1);
-    meshes[5].setEffectAssociation(0,32,0);
-
-    mFaceMesh = new MeshJoined(meshes);
-
-    Static3D move0  = new Static3D(-1.0f/8, -1.0f/8, 0);
-    Static3D move1  = new Static3D(-(SQ2/24)-1.0f/4, -(SQ2/24)-1.0f/4, 0);
-    Static3D move2  = new Static3D(-E/2, F/3, 0);
-    Static3D move3  = new Static3D(+E/2, F/3, 0);
-    Static3D move4  = new Static3D(+E/3,+E/3, 0);
-    Static1D angle1 = new Static1D(135);
-    Static1D angle2 = new Static1D(90);
-    Static1D angle3 = new Static1D(-90);
-    Static1D angle4 = new Static1D(-135);
-    Static3D axisX  = new Static3D(1,0,0);
-    Static3D axisY  = new Static3D(0,1,0);
-    Static3D axisZ  = new Static3D(0,0,1);
-    Static3D axis1  = new Static3D(1,-1,0);
-    Static3D center = new Static3D(0,0,0);
-    Static3D center1= new Static3D(-0.25f,-0.25f,0);
-
-    float d0 =-0.03f;
-    float d1 =-0.04f;
-    float r0 = 0.15f;
-    float r1 = 0.10f;
-
-    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
-    Static3D vec1   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(+SQ3/3));
-    Static3D vec2   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
-
-    Static1D radius = new Static1D(0.5f);
-
-    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
-    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
-    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
-
-    Static4D reg0   = new Static4D(0,0,0,r0);
-    Static4D reg1   = new Static4D(0,0,0,r1);
-
-    VertexEffectMove   effect0 = new VertexEffectMove(move0);
-    VertexEffectRotate effect1 = new VertexEffectRotate(angle1, axisZ, center);
-    VertexEffectMove   effect2 = new VertexEffectMove(move1);
-    VertexEffectRotate effect3 = new VertexEffectRotate(angle2, axis1, center1);
-    VertexEffectMove   effect4 = new VertexEffectMove(move2);
-    VertexEffectMove   effect5 = new VertexEffectMove(move3);
-    VertexEffectRotate effect6 = new VertexEffectRotate(angle3, axisZ, center);
-    VertexEffectRotate effect7 = new VertexEffectRotate(angle4, axisX, center);
-    VertexEffectRotate effect8 = new VertexEffectRotate(angle1, axisY, center);
-    VertexEffectScale  effect9 = new VertexEffectScale(0);
-    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
-    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
-    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
-    VertexEffectMove   effect13= new VertexEffectMove(move4);
-
-    effect0.setMeshAssociation( 1,-1);  // mesh 0
-    effect1.setMeshAssociation( 2,-1);  // mesh 1
-    effect2.setMeshAssociation( 2,-1);  // mesh 1
-    effect3.setMeshAssociation( 2,-1);  // mesh 1
-    effect4.setMeshAssociation( 4,-1);  // mesh 2
-    effect5.setMeshAssociation( 8,-1);  // mesh 3
-    effect6.setMeshAssociation( 8,-1);  // mesh 3
-    effect7.setMeshAssociation( 4,-1);  // mesh 2
-    effect8.setMeshAssociation( 8,-1);  // mesh 3
-    effect9.setMeshAssociation(48,-1);  // meshes 4,5
-    effect10.setMeshAssociation(15,-1); // meshes 0,1,2,3
-    effect11.setMeshAssociation(15,-1); // meshes 0,1,2,3
-    effect12.setMeshAssociation(15,-1); // meshes 0,1,2,3
-    effect13.setMeshAssociation(15,-1); // meshes 0,1,2,3
-
-    mFaceMesh.apply(effect0);
-    mFaceMesh.apply(effect1);
-    mFaceMesh.apply(effect2);
-    mFaceMesh.apply(effect3);
-    mFaceMesh.apply(effect4);
-    mFaceMesh.apply(effect5);
-    mFaceMesh.apply(effect6);
-    mFaceMesh.apply(effect7);
-    mFaceMesh.apply(effect8);
-    mFaceMesh.apply(effect9);
-    mFaceMesh.apply(effect10);
-    mFaceMesh.apply(effect11);
-    mFaceMesh.apply(effect12);
-    mFaceMesh.apply(effect13);
-
-    mFaceMesh.mergeEffComponents();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 1.5f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static4D[] getQuats()
-    {
-    return QUATS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumStickerTypes()
-    {
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getBasicStep()
-    {
-    return SQ2/2;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumCubitFaces()
-    {
-    return FACES_PER_CUBIT;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static3D[] getCubitPositions(int size)
-    {
-    return CENTERS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int cubit)
-    {
-    MeshBase mesh;
-
-    if( cubit<8 )
-      {
-      if( mCornerMesh==null ) createCornerMesh();
-      mesh = mCornerMesh.copy(true);
-      }
-    else
-      {
-      if( mFaceMesh==null ) createFaceMesh();
-      mesh = mFaceMesh.copy(true);
-      }
-
-    int index = QUAT_INDICES[cubit];
-    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( QUATS[index], new Static3D(0,0,0) );
-    mesh.apply(quat,0xffffffff,0);
-
-    return mesh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    return mFaceMap[cubit][cubitface];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
-    {
-    float STROKE = 0.035f*side;
-    float L= left+0.125f*side;
-    float H= 0.375f*side;
-    float LEN = 0.5f*side;
-
-    paint.setAntiAlias(true);
-    paint.setStrokeWidth(STROKE);
-    paint.setColor(FACE_COLORS[face]);
-    paint.setStyle(Paint.Style.FILL);
-
-    canvas.drawRect(left,top,left+side,top+side,paint);
-
-    paint.setColor(INTERIOR_COLOR);
-    paint.setStyle(Paint.Style.STROKE);
-
-    canvas.drawLine( L    , H,  L+LEN, H    , paint);
-    canvas.drawLine( L    , H,  L+LEN, H+LEN, paint);
-    canvas.drawLine( L+LEN, H,  L+LEN, H+LEN, paint);
-
-    float S1 = 0.125f*side;
-    float S2 = 0.070f*side;
-    float X  = 0.7f*S2;
-
-    float LA = left+0.625f*side;
-    float RA = left+0.125f*side;
-    float TA = 0.375f*side;
-    float BA = 0.875f*side;
-
-    canvas.drawArc( LA-S1, TA     , LA     , TA+S1, 270, 90, false, paint);
-    canvas.drawArc( RA+X , TA     , RA+X+S2, TA+S2, 135,135, false, paint);
-    canvas.drawArc( LA-S2, BA-X-S2, LA     , BA-X ,   0,135, false, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return 2.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getRowChances()
-    {
-    float[] chances = new float[3];
-
-    chances[0] = 0.5f;
-    chances[1] = 0.5f;
-    chances[2] = 1.0f;
-
-    return chances;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public Static3D[] getRotationAxis()
-    {
-    return ROT_AXIS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getBasicAngle()
-    {
-    return 2;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeRowFromOffset(float offset)
-    {
-    return offset<0.166f ? 0:2;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(float offset)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
-    {
-    int numAxis = ROTATION_AXIS.length;
-
-    if( oldRotAxis == START_AXIS )
-      {
-      return rnd.nextInt(numAxis);
-      }
-    else
-      {
-      int newVector = rnd.nextInt(numAxis-2);
-
-      switch(oldRotAxis)
-        {
-        case  0:
-        case  1: return newVector+2;
-        case  2:
-        case  3: return (newVector==0 || newVector==1) ? newVector:newVector+2;
-        default: return newVector;
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    float rowFloat = rnd.nextFloat();
-
-    for(int row=0; row<mRowChances.length; row++)
-      {
-      if( rowFloat<=mRowChances[row] ) return row;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// remember about the double cover or unit quaternions!
-
-  private int mulQuat(int q1, int q2)
-    {
-    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
-
-    float rX = result.get0();
-    float rY = result.get1();
-    float rZ = result.get2();
-    float rW = result.get3();
-
-    final float MAX_ERROR = 0.1f;
-    float dX,dY,dZ,dW;
-
-    for(int i=0; i<QUATS.length; i++)
-      {
-      dX = QUATS[i].get0() - rX;
-      dY = QUATS[i].get1() - rY;
-      dZ = QUATS[i].get2() - rZ;
-      dW = QUATS[i].get3() - rW;
-
-      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
-          dY<MAX_ERROR && dY>-MAX_ERROR &&
-          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
-          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
-
-      dX = QUATS[i].get0() + rX;
-      dY = QUATS[i].get1() + rY;
-      dZ = QUATS[i].get2() + rZ;
-      dW = QUATS[i].get3() + rW;
-
-      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
-          dY<MAX_ERROR && dY>-MAX_ERROR &&
-          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
-          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// The Helicopter is solved if and only if:
-//
-// 1) all of its corner cubits are rotated with the same quat
-// 2) all its face cubits are rotated with the same quat like the corner ones,
-//    and optionally they also might be turned by a multiple of 90 degrees along
-//    a vector perpendicular to the face they lie on.
-//
-// i.e.
-// cubits  8, 9,10,11,12,13,14,15 - might be extra QUAT 1,8,9
-// cubits 16,17,18,19,20,21,22,23 - might be extra QUAT 2,12,13
-// cubits 24,25,26,27,28,29,30,31 - might be extra QUAT 3,14,15
-
-  public boolean isSolved()
-    {
-    int q = CUBITS[0].mQuatIndex;
-
-    if ( CUBITS[1].mQuatIndex == q &&
-         CUBITS[2].mQuatIndex == q &&
-         CUBITS[3].mQuatIndex == q &&
-         CUBITS[4].mQuatIndex == q &&
-         CUBITS[5].mQuatIndex == q &&
-         CUBITS[6].mQuatIndex == q &&
-         CUBITS[7].mQuatIndex == q  )
-      {
-      int q1 = mulQuat(q,1);
-      int q2 = mulQuat(q,8);
-      int q3 = mulQuat(q,9);
-
-      for(int index=8; index<16; index++)
-        {
-        int qIndex = CUBITS[index].mQuatIndex;
-        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
-        }
-
-      q1 = mulQuat(q, 2);
-      q2 = mulQuat(q,12);
-      q3 = mulQuat(q,13);
-
-      for(int index=16; index<24; index++)
-        {
-        int qIndex = CUBITS[index].mQuatIndex;
-        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
-        }
-
-      q1 = mulQuat(q, 3);
-      q2 = mulQuat(q,14);
-      q3 = mulQuat(q,15);
-
-      for(int index=24; index<32; index++)
-        {
-        int qIndex = CUBITS[index].mQuatIndex;
-        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
-        }
-
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// only needed for solvers - there are no Helicopter solvers ATM)
-
-  public String retObjectString()
-    {
-    return "";
-    }
-
-}
diff --git a/src/main/java/org/distorted/objects/RubikObject.java b/src/main/java/org/distorted/objects/RubikObject.java
deleted file mode 100644
index 6f46302f..00000000
--- a/src/main/java/org/distorted/objects/RubikObject.java
+++ /dev/null
@@ -1,718 +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.objects;
-
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.library.effect.Effect;
-import org.distorted.library.effect.MatrixEffectMove;
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.effect.MatrixEffectScale;
-import org.distorted.library.effect.VertexEffectQuaternion;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedNode;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshFile;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.type.Dynamic1D;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Random;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class RubikObject extends DistortedNode
-  {
-  static final int COLOR_YELLOW = 0xffffff00;
-  static final int COLOR_WHITE  = 0xffffffff;
-  static final int COLOR_BLUE   = 0xff0000ff;
-  static final int COLOR_GREEN  = 0xff00ff00;
-  static final int COLOR_RED    = 0xffff0000;
-  static final int COLOR_BROWN  = 0xffb5651d;
-  static final int COLOR_PINK   = 0xffe134eb;
-  static final int COLOR_VIOLET = 0xffa534eb;
-
-  private static final float NODE_RATIO = 1.32f;
-  private static final float MAX_SIZE_CHANGE = 1.3f;
-  private static final float MIN_SIZE_CHANGE = 0.8f;
-
-  private static boolean mCreateFromDMesh = true;
-
-  private static final Static3D CENTER = new Static3D(0,0,0);
-  static final int INTERIOR_COLOR = 0xff000000;
-  private static final int POST_ROTATION_MILLISEC = 500;
-  private static final int TEXTURE_HEIGHT = 256;
-
-  final Static3D[] ROTATION_AXIS;
-  final Static4D[] QUATS;
-  final Cubit[] CUBITS;
-  final int NUM_FACES;
-  final int NUM_TEXTURES;
-  final int NUM_CUBIT_FACES;
-  final int NUM_AXIS;
-  final int NUM_CUBITS;
-  final float BASIC_STEP;
-
-  private static float mInitScreenRatio;
-  private static float mObjectScreenRatio = 1.0f;
-
-  private final int mNodeSize;
-  private int mRotRowBitmap;
-  private int mRotAxis;
-  private Static3D[] mOrigPos;
-  private Static3D mNodeScale;
-  private Static4D mQuat;
-  private int mSize;
-  private RubikObjectList mList;
-  private MeshBase mMesh;
-  private DistortedEffects mEffects;
-  private VertexEffectRotate mRotateEffect;
-  private Dynamic1D mRotationAngle;
-  private Static3D mRotationAxis;
-  private Static3D mObjectScale;
-
-  float mStart, mStep;
-  float[] mRowChances;
-
-  Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
-  DistortedTexture mTexture;
-
-  MatrixEffectScale mScaleEffect;
-  MatrixEffectQuaternion mQuatEffect;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikObject(int size, int fov, Static4D quat, DistortedTexture nodeTexture, MeshSquare nodeMesh,
-              DistortedEffects nodeEffects, int[][] moves, RubikObjectList list, Resources res, int screenWidth)
-    {
-    super(nodeTexture,nodeEffects,nodeMesh);
-
-    mNodeSize = screenWidth;
-
-    resizeFBO(mNodeSize, (int)(NODE_RATIO*mNodeSize));
-
-    mList = list;
-    mOrigPos = getCubitPositions(size);
-
-    QUATS = getQuats();
-    NUM_CUBITS  = mOrigPos.length;
-    ROTATION_AXIS = getRotationAxis();
-    NUM_AXIS = ROTATION_AXIS.length;
-    mInitScreenRatio = getScreenRatio();
-    NUM_FACES = getNumFaces();
-    NUM_CUBIT_FACES = getNumCubitFaces();
-    NUM_TEXTURES = getNumStickerTypes()*NUM_FACES;
-    BASIC_STEP = getBasicStep();
-
-    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
-    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
-
-    mSize = size;
-    computeStartAndStep(mOrigPos);
-    mNodeScale= new Static3D(1,NODE_RATIO,1);
-    mQuat = quat;
-
-    mRowChances = getRowChances();
-
-    mRotationAngle= new Dynamic1D();
-    mRotationAxis = new Static3D(1,0,0);
-    mRotateEffect = new VertexEffectRotate(mRotationAngle, mRotationAxis, CENTER);
-
-    mRotationAngleStatic = new Static1D(0);
-    mRotationAngleMiddle = new Static1D(0);
-    mRotationAngleFinal  = new Static1D(0);
-
-    float scale  = mObjectScreenRatio*mInitScreenRatio*mNodeSize/mSize;
-    mObjectScale = new Static3D(scale,scale,scale);
-    mScaleEffect = new MatrixEffectScale(mObjectScale);
-    mQuatEffect  = new MatrixEffectQuaternion(quat, CENTER);
-
-    MatrixEffectScale nodeScaleEffect = new MatrixEffectScale(mNodeScale);
-    nodeEffects.apply(nodeScaleEffect);
-
-    CUBITS = new Cubit[NUM_CUBITS];
-    createMeshAndCubits(list,res);
-
-    mTexture = new DistortedTexture();
-    mEffects = new DistortedEffects();
-
-    int num_quats = QUATS.length;
-    for(int q=0; q<num_quats; q++)
-      {
-      VertexEffectQuaternion vq = new VertexEffectQuaternion(QUATS[q],CENTER);
-      vq.setMeshAssociation(0,q);
-      mEffects.apply(vq);
-      }
-
-    mEffects.apply(mRotateEffect);
-    mEffects.apply(mQuatEffect);
-    mEffects.apply(mScaleEffect);
-
-    // Now postprocessed effects (the glow when you solve an object) require component centers. In
-    // order for the effect to be in front of the object, we need to set the center to be behind it.
-    getMesh().setComponentCenter(0,0,0,-0.1f);
-
-    attach( new DistortedNode(mTexture,mEffects,mMesh) );
-
-    setupPosition(moves);
-
-    setProjection(fov, 0.1f);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createMeshAndCubits(RubikObjectList list, Resources res)
-    {
-    if( mCreateFromDMesh )
-      {
-      int sizeIndex = RubikObjectList.getSizeIndex(list.ordinal(),mSize);
-      int resourceID= list.getResourceIDs()[sizeIndex];
-
-      InputStream is = res.openRawResource(resourceID);
-      DataInputStream dos = new DataInputStream(is);
-      mMesh = new MeshFile(dos);
-
-      try
-        {
-        is.close();
-        }
-      catch(IOException e)
-        {
-        android.util.Log.e("meshFile", "Error closing InputStream: "+e.toString());
-        }
-
-      for(int i=0; i<NUM_CUBITS; i++)
-        {
-        CUBITS[i] = new Cubit(this,mOrigPos[i]);
-        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(), 0);
-        }
-
-      if( shouldResetTextureMaps() ) resetAllTextureMaps();
-      }
-    else
-      {
-      MeshBase[] cubitMesh = new MeshBase[NUM_CUBITS];
-
-      for(int i=0; i<NUM_CUBITS; i++)
-        {
-        CUBITS[i] = new Cubit(this,mOrigPos[i]);
-        cubitMesh[i] = createCubitMesh(i);
-        cubitMesh[i].apply(new MatrixEffectMove(mOrigPos[i]),1,0);
-        cubitMesh[i].setEffectAssociation(0, CUBITS[i].computeAssociation(), 0);
-        }
-
-      mMesh = new MeshJoined(cubitMesh);
-      resetAllTextureMaps();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setObjectRatio(float sizeChange)
-    {
-    mObjectScreenRatio *= (1.0f+sizeChange)/2;
-
-    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
-    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
-
-    float scale = mObjectScreenRatio*mInitScreenRatio*mNodeSize/mSize;
-    mObjectScale.set(scale,scale,scale);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static float getObjectRatio()
-    {
-    return mObjectScreenRatio*mInitScreenRatio;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Cast centers of all Cubits on the first rotation Axis and compute the leftmost and rightmost
-// one. From there compute the 'start' (i.e. the leftmost) and 'step' (i.e. distance between two
-// consecutive).
-// it is assumed that other rotation axis have the same 'start' and 'step' - this is the case with
-// the Cube and the Pyraminx.
-// Start and Step are then needed to compute which rotation row (with respect to a given axis) a
-// given Cubit belongs to.
-
-  private void computeStartAndStep(Static3D[] pos)
-    {
-    float min = Float.MAX_VALUE;
-    float max = Float.MIN_VALUE;
-    float axisX = ROTATION_AXIS[0].get0();
-    float axisY = ROTATION_AXIS[0].get1();
-    float axisZ = ROTATION_AXIS[0].get2();
-    float tmp;
-
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      tmp = pos[i].get0()*axisX + pos[i].get1()*axisY + pos[i].get2()*axisZ;
-      if( tmp<min ) min=tmp;
-      if( tmp>max ) max=tmp;
-      }
-
-    mStart = min;
-    mStep  = (max-min+BASIC_STEP)/mSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean belongsToRotation( int cubit, int axis, int rowBitmap)
-    {
-    int cubitRow = (int)(CUBITS[cubit].mRotationRow[axis]+0.5f);
-    return ((1<<cubitRow)&rowBitmap)!=0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// note the minus in front of the sin() - we rotate counterclockwise
-// when looking towards the direction where the axis increases in values.
-
-  private Static4D makeQuaternion(int axisIndex, int angleInDegrees)
-    {
-    Static3D axis = ROTATION_AXIS[axisIndex];
-
-    while( angleInDegrees<0 ) angleInDegrees += 360;
-    angleInDegrees %= 360;
-    
-    float cosA = (float)Math.cos(Math.PI*angleInDegrees/360);
-    float sinA =-(float)Math.sqrt(1-cosA*cosA);
-
-    return new Static4D(axis.get0()*sinA, axis.get1()*sinA, axis.get2()*sinA, cosA);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private synchronized void setupPosition(int[][] moves)
-    {
-    if( moves!=null )
-      {
-      Static4D quat;
-      int index, axis, rowBitmap, angle;
-      int corr = (360/getBasicAngle());
-
-      for(int[] move: moves)
-        {
-        axis     = move[0];
-        rowBitmap= move[1];
-        angle    = move[2]*corr;
-        quat     = makeQuaternion(axis,angle);
-
-        for(int j=0; j<NUM_CUBITS; j++)
-          if( belongsToRotation(j,axis,rowBitmap) )
-            {
-            index = CUBITS[j].removeRotationNow(quat);
-            mMesh.setEffectAssociation(j, CUBITS[j].computeAssociation(),index);
-            }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// we cannot use belongsToRotation for deciding if to texture a face. Counterexample: the 'rotated'
-// tetrahedrons of Pyraminx nearby the edge: they belong to rotation but their face which is rotated
-// away from the face of the Pyraminx shouldn't be textured.
-
-  boolean isOnFace( int cubit, int axis, int row)
-    {
-    final float MAX_ERROR = 0.0001f;
-    float diff = CUBITS[cubit].mRotationRow[axis] - row;
-    return diff*diff < MAX_ERROR;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getCubitFaceColorIndex(int cubit, int face)
-    {
-    Static4D texMap = mMesh.getTextureMap(NUM_FACES*cubit + face);
-    return (int)(texMap.get0() / texMap.get2());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Clamp all rotated positions to one of those original ones to avoid accumulating errors.
-
-  void clampPos(Static3D pos)
-    {
-    float currError, minError = Float.MAX_VALUE;
-    int minErrorIndex= -1;
-    float x = pos.get0();
-    float y = pos.get1();
-    float z = pos.get2();
-    float xo,yo,zo;
-
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      xo = mOrigPos[i].get0();
-      yo = mOrigPos[i].get1();
-      zo = mOrigPos[i].get2();
-
-      currError = (xo-x)*(xo-x) + (yo-y)*(yo-y) + (zo-z)*(zo-z);
-
-      if( currError<minError )
-        {
-        minError = currError;
-        minErrorIndex = i;
-        }
-      }
-
-    pos.set( mOrigPos[minErrorIndex] );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// the getFaceColors + final black in a horizontal strip.
-
-  public void createTexture()
-    {
-    Bitmap bitmap;
-
-    Paint paint = new Paint();
-    bitmap = Bitmap.createBitmap( (NUM_TEXTURES+1)*TEXTURE_HEIGHT, TEXTURE_HEIGHT, Bitmap.Config.ARGB_8888);
-    Canvas canvas = new Canvas(bitmap);
-
-    paint.setAntiAlias(true);
-    paint.setTextAlign(Paint.Align.CENTER);
-    paint.setStyle(Paint.Style.FILL);
-
-    paint.setColor(INTERIOR_COLOR);
-    canvas.drawRect(0, 0, (NUM_TEXTURES+1)*TEXTURE_HEIGHT, TEXTURE_HEIGHT, paint);
-
-    for(int i=0; i<NUM_TEXTURES; i++)
-      {
-      createFaceTexture(canvas, paint, i, i*TEXTURE_HEIGHT, 0, TEXTURE_HEIGHT);
-      }
-
-    mTexture.setTexture(bitmap);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getSize()
-    {
-    return mSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void continueRotation(float angleInDegrees)
-    {
-    mRotationAngleStatic.set0(angleInDegrees);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Static4D getRotationQuat()
-      {
-      return mQuat;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void recomputeScaleFactor(int scrWidth, int scrHeight)
-    {
-    mNodeScale.set(scrWidth,NODE_RATIO*scrWidth,scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-    for(int i=0; i<NUM_CUBITS; i++) CUBITS[i].savePreferences(editor);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void restorePreferences(SharedPreferences preferences)
-    {
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      int index = CUBITS[i].restorePreferences(preferences);
-      mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),index);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void releaseResources()
-    {
-    mTexture.markForDeletion();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void apply(Effect effect, int position)
-    {
-    mEffects.apply(effect, position);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void remove(long effectID)
-    {
-    mEffects.abortById(effectID);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void solve()
-    {
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      CUBITS[i].solve();
-      mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(), 0);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void resetAllTextureMaps()
-    {
-    final float ratio = 1.0f/(NUM_TEXTURES+1);
-    int color;
-
-    for(int cubit=0; cubit<NUM_CUBITS; cubit++)
-      {
-      final Static4D[] maps = new Static4D[NUM_CUBIT_FACES];
-
-      for(int cubitface=0; cubitface<NUM_CUBIT_FACES; cubitface++)
-        {
-        color = getFaceColor(cubit,cubitface,mSize);
-        maps[cubitface] = new Static4D( color*ratio, 0.0f, ratio, 1.0f);
-        }
-
-      mMesh.setTextureMap(maps,NUM_CUBIT_FACES*cubit);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setTextureMap(int cubit, int face, int newColor)
-    {
-    final float ratio = 1.0f/(NUM_TEXTURES+1);
-    final Static4D[] maps = new Static4D[NUM_CUBIT_FACES];
-
-    maps[face] = new Static4D( newColor*ratio, 0.0f, ratio, 1.0f);
-    mMesh.setTextureMap(maps,NUM_CUBIT_FACES*cubit);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void beginNewRotation(int axis, int row )
-    {
-    if( axis<0 || axis>=ROTATION_AXIS.length )
-      {
-      android.util.Log.e("object", "invalid rotation axis: "+axis);
-      return;
-      }
-    if( row<0 || row>=mSize )
-      {
-      android.util.Log.e("object", "invalid rotation row: "+row);
-      return;
-      }
-
-    mRotAxis     = axis;
-    mRotRowBitmap= (1<<row);
-    mRotationAngleStatic.set0(0.0f);
-    mRotationAxis.set( ROTATION_AXIS[axis] );
-    mRotationAngle.add(mRotationAngleStatic);
-    mRotateEffect.setMeshAssociation( mRotRowBitmap<<(axis*RubikObjectList.MAX_OBJECT_SIZE) , -1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized long addNewRotation( int axis, int rowBitmap, int angle, long durationMillis, EffectListener listener )
-    {
-    mRotAxis     = axis;
-    mRotRowBitmap= rowBitmap;
-
-    mRotationAngleStatic.set0(0.0f);
-    mRotationAxis.set( ROTATION_AXIS[axis] );
-    mRotationAngle.setDuration(durationMillis);
-    mRotationAngle.resetToBeginning();
-    mRotationAngle.add(new Static1D(0));
-    mRotationAngle.add(new Static1D(angle));
-    mRotateEffect.setMeshAssociation( mRotRowBitmap<<(axis*RubikObjectList.MAX_OBJECT_SIZE) , -1);
-    mRotateEffect.notifyWhenFinished(listener);
-
-    return mRotateEffect.getID();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public long finishRotationNow(EffectListener listener, int nearestAngleInDegrees)
-    {
-    float angle = getAngle();
-    mRotationAngleStatic.set0(angle);
-    mRotationAngleFinal.set0(nearestAngleInDegrees);
-    mRotationAngleMiddle.set0( nearestAngleInDegrees + (nearestAngleInDegrees-angle)*0.2f );
-
-    mRotationAngle.setDuration(POST_ROTATION_MILLISEC);
-    mRotationAngle.resetToBeginning();
-    mRotationAngle.removeAll();
-    mRotationAngle.add(mRotationAngleStatic);
-    mRotationAngle.add(mRotationAngleMiddle);
-    mRotationAngle.add(mRotationAngleFinal);
-    mRotateEffect.notifyWhenFinished(listener);
-
-    return mRotateEffect.getID();
-    }
-
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float getAngle()
-    {
-    int pointNum = mRotationAngle.getNumPoints();
-
-    if( pointNum>=1 )
-      {
-      return mRotationAngle.getPoint(pointNum-1).get0();
-      }
-    else
-      {
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.log("points in RotationAngle: "+pointNum);
-      return 0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void removeRotationNow()
-    {
-    float angle = getAngle();
-    double nearestAngleInRadians = angle*Math.PI/180;
-    float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
-    float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
-    float axisX = ROTATION_AXIS[mRotAxis].get0();
-    float axisY = ROTATION_AXIS[mRotAxis].get1();
-    float axisZ = ROTATION_AXIS[mRotAxis].get2();
-    Static4D quat = new Static4D( axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
-
-    mRotationAngle.removeAll();
-    mRotationAngleStatic.set0(0);
-
-    for(int i=0; i<NUM_CUBITS; i++)
-      if( belongsToRotation(i,mRotAxis,mRotRowBitmap) )
-        {
-        int index = CUBITS[i].removeRotationNow(quat);
-        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),index);
-        }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void initializeObject(int[][] moves)
-    {
-    solve();
-    setupPosition(moves);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCubit(float[] point3D)
-    {
-    float dist, minDist = Float.MAX_VALUE;
-    int currentBest=-1;
-    float multiplier = returnMultiplier();
-
-    point3D[0] *= multiplier;
-    point3D[1] *= multiplier;
-    point3D[2] *= multiplier;
-
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      dist = CUBITS[i].getDistSquared(point3D);
-      if( dist<minDist )
-        {
-        minDist = dist;
-        currentBest = i;
-        }
-      }
-
-    return currentBest;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeNearestAngle(float angle, float speed)
-    {
-    final int NEAREST = 360/getBasicAngle();
-
-    int tmp = (int)((angle+NEAREST/2)/NEAREST);
-    if( angle< -(NEAREST*0.5) ) tmp-=1;
-
-    if( tmp!=0 ) return NEAREST*tmp;
-
-    return speed> 1.2f ? NEAREST*(angle>0 ? 1:-1) : 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNodeSize()
-    {
-    return mNodeSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public RubikObjectList getObjectList()
-    {
-    return mList;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract float getScreenRatio();
-  abstract Static3D[] getCubitPositions(int size);
-  abstract Static4D[] getQuats();
-  abstract int getNumFaces();
-  abstract int getNumStickerTypes();
-  abstract int getNumCubitFaces();
-  abstract MeshBase createCubitMesh(int cubit);
-  abstract void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side);
-  abstract int getFaceColor(int cubit, int cubitface, int size);
-  abstract float returnMultiplier();
-  abstract float[] getRowChances();
-  abstract float getBasicStep();
-  abstract boolean shouldResetTextureMaps();
-
-  public abstract boolean isSolved();
-  public abstract Static3D[] getRotationAxis();
-  public abstract int getBasicAngle();
-  public abstract int computeRowFromOffset(float offset);
-  public abstract float returnRotationFactor(float offset);
-  public abstract String retObjectString();
-  public abstract int randomizeNewRotAxis(Random rnd, int oldRotAxis);
-  public abstract int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis);
-  }
diff --git a/src/main/java/org/distorted/objects/RubikObjectList.java b/src/main/java/org/distorted/objects/RubikObjectList.java
deleted file mode 100644
index b392f6b6..00000000
--- a/src/main/java/org/distorted/objects/RubikObjectList.java
+++ /dev/null
@@ -1,494 +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.objects;
-
-import android.content.res.Resources;
-
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.R;
-import org.distorted.main.RubikActivity;
-
-import java.lang.reflect.Field;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public enum RubikObjectList
-  {
-  CUBE (
-         new int[][] {
-                       {2 , 12, R.raw.cube2, R.drawable.ui_small_cube2, R.drawable.ui_medium_cube2, R.drawable.ui_big_cube2, R.drawable.ui_huge_cube2} ,
-                       {3 , 16, R.raw.cube3, R.drawable.ui_small_cube3, R.drawable.ui_medium_cube3, R.drawable.ui_big_cube3, R.drawable.ui_huge_cube3} ,
-                       {4 , 20, R.raw.cube4, R.drawable.ui_small_cube4, R.drawable.ui_medium_cube4, R.drawable.ui_big_cube4, R.drawable.ui_huge_cube4} ,
-                       {5 , 24, R.raw.cube5, R.drawable.ui_small_cube5, R.drawable.ui_medium_cube5, R.drawable.ui_big_cube5, R.drawable.ui_huge_cube5}
-                     },
-         RubikCube.class,
-         new MovementCube(),
-         0
-       ),
-
-  PYRA (
-         new int[][] {
-                       {3 , 10, R.raw.pyra3, R.drawable.ui_small_pyra3, R.drawable.ui_medium_pyra3, R.drawable.ui_big_pyra3, R.drawable.ui_huge_pyra3} ,
-                       {4 , 15, R.raw.pyra4, R.drawable.ui_small_pyra4, R.drawable.ui_medium_pyra4, R.drawable.ui_big_pyra4, R.drawable.ui_huge_pyra4} ,
-                       {5 , 20, R.raw.pyra5, R.drawable.ui_small_pyra5, R.drawable.ui_medium_pyra5, R.drawable.ui_big_pyra5, R.drawable.ui_huge_pyra5}
-                     },
-         RubikPyraminx.class,
-         new MovementPyraminx(),
-         1
-       ),
-
-  DIAM (
-         new int[][] {
-                       {2 , 10, R.raw.dino, R.drawable.ui_small_dino, R.drawable.ui_medium_dino, R.drawable.ui_big_dino, R.drawable.ui_huge_dino} ,
-                     },
-         RubikDiamond.class,
-         new MovementDiamond(),
-         1
-       ),
-
-  DINO (
-         new int[][] {
-                       {3 , 10, R.raw.dino, R.drawable.ui_small_dino, R.drawable.ui_medium_dino, R.drawable.ui_big_dino, R.drawable.ui_huge_dino} ,
-                     },
-         RubikDino6.class,
-         new MovementDino(),
-         2
-       ),
-
-  DIN4 (
-         new int[][] {
-                       {3 ,  7, R.raw.dino, R.drawable.ui_small_din4, R.drawable.ui_medium_din4, R.drawable.ui_big_din4, R.drawable.ui_huge_din4} ,
-                     },
-         RubikDino4.class,
-         new MovementDino(),
-         2
-       ),
-
-  SKEW (
-         new int[][] {
-                       {2 , 11, R.raw.skewb, R.drawable.ui_small_skewb, R.drawable.ui_medium_skewb, R.drawable.ui_big_skewb, R.drawable.ui_huge_skewb} ,
-                     },
-         RubikSkewb.class,
-         new MovementSkewb(),
-         2
-       ),
-
-  HELI (
-         new int[][] {
-                       {3 , 18, R.raw.heli, R.drawable.ui_small_heli, R.drawable.ui_medium_heli, R.drawable.ui_big_heli, R.drawable.ui_huge_heli} ,
-                     },
-         RubikHelicopter.class,
-         new MovementHelicopter(),
-         2
-       ),
-  ;
-
-  public static final int NUM_OBJECTS = values().length;
-  public static final int MAX_NUM_OBJECTS;
-  public static final int MAX_LEVEL;
-  public static final int MAX_OBJECT_SIZE;
-
-  private final int[] mObjectSizes, mMaxLevels, mSmallIconIDs, mMediumIconIDs, mBigIconIDs, mHugeIconIDs, mResourceIDs;
-  private final Class<? extends RubikObject> mObjectClass;
-  private final Movement mObjectMovementClass;
-  private final int mColumn, mNumSizes;
-
-  private static final RubikObjectList[] objects;
-  private static int mNumAll;
-  private static int[] mIndices;
-  private static int mColCount, mRowCount;
-
-  static
-    {
-    mNumAll = 0;
-    int num, i = 0;
-    objects = new RubikObjectList[NUM_OBJECTS];
-    int maxNum  = Integer.MIN_VALUE;
-    int maxLevel= Integer.MIN_VALUE;
-    int maxSize = Integer.MIN_VALUE;
-
-    for(RubikObjectList object: RubikObjectList.values())
-      {
-      objects[i] = object;
-      i++;
-      num = object.mObjectSizes.length;
-      mNumAll += num;
-      if( num> maxNum ) maxNum = num;
-
-      for(int j=0; j<num; j++)
-        {
-        if( object.mMaxLevels[j] > maxLevel ) maxLevel = object.mMaxLevels[j];
-        if( object.mObjectSizes[j] > maxSize) maxSize  = object.mObjectSizes[j];
-        }
-      }
-
-    MAX_NUM_OBJECTS = maxNum;
-    MAX_LEVEL       = maxLevel;
-    MAX_OBJECT_SIZE = maxSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void setUpColAndRow()
-    {
-    mIndices = new int[NUM_OBJECTS];
-    mColCount= 0;
-
-    for(int obj=0; obj<NUM_OBJECTS; obj++)
-      {
-      mIndices[obj] = objects[obj].mColumn;
-      if( mIndices[obj]>=mColCount ) mColCount = mIndices[obj]+1;
-      }
-
-    mRowCount = 0;
-
-    for(int col=0; col<mColCount; col++)
-      {
-      int numObjects = computeNumObjectsInColumn(col);
-      if( numObjects>mRowCount ) mRowCount = numObjects;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int computeNumObjectsInColumn(int column)
-    {
-    int num=0;
-
-    for(int object=0; object<NUM_OBJECTS; object++)
-      {
-      if( objects[object].mColumn == column )
-        {
-        num += objects[object].mNumSizes;
-        }
-      }
-
-    return num;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getColumnCount()
-    {
-    if( mIndices==null ) setUpColAndRow();
-
-    return mColCount;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getRowCount()
-    {
-    if( mIndices==null ) setUpColAndRow();
-
-    return mRowCount;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int[] getIndices()
-    {
-    if( mIndices==null ) setUpColAndRow();
-
-    return mIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static RubikObjectList getObject(int ordinal)
-    {
-    return ordinal>=0 && ordinal<NUM_OBJECTS ? objects[ordinal] : CUBE;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int pack(int object, int sizeIndex)
-    {
-    int ret = 0;
-    for(int i=0; i<object; i++) ret += objects[i].mObjectSizes.length;
-
-    return ret+sizeIndex;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int unpackSizeIndex(int number)
-    {
-    int num;
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      {
-      num = objects[i].mObjectSizes.length;
-      if( number<num ) return number;
-      number -= num;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int unpackObject(int number)
-    {
-    int num;
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      {
-      num = objects[i].mObjectSizes.length;
-      if( number<num ) return i;
-      number -= num;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int unpackObjectFromString(String obj)
-    {
-    int u = obj.indexOf('_');
-    int l = obj.length();
-
-    if( u>0 )
-      {
-      String name = obj.substring(0,u);
-      int size = Integer.parseInt( obj.substring(u+1,l) );
-
-      for(int i=0; i<NUM_OBJECTS; i++)
-        {
-        if( objects[i].name().equals(name) )
-          {
-          int sizeIndex = getSizeIndex(i,size);
-          return pack(i,sizeIndex);
-          }
-        }
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static String getObjectList()
-    {
-    String name;
-    StringBuilder list = new StringBuilder();
-    int len;
-    int[] sizes;
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      {
-      sizes = objects[i].mObjectSizes;
-      len   = sizes.length;
-      name  = objects[i].name();
-
-      for(int j=0; j<len; j++)
-        {
-        if( i>0 || j>0 ) list.append(',');
-        list.append(name);
-        list.append('_');
-        list.append(sizes[j]);
-        }
-      }
-
-    return list.toString();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getTotal()
-    {
-    return mNumAll;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getMaxLevel(int ordinal, int sizeIndex)
-    {
-    if( ordinal>=0 && ordinal<NUM_OBJECTS )
-      {
-      int num = objects[ordinal].mObjectSizes.length;
-      return sizeIndex>=0 && sizeIndex<num ? objects[ordinal].mMaxLevels[sizeIndex] : 0;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getOrdinal(String name)
-    {
-    for(int i=0; i<NUM_OBJECTS; i++)
-      {
-      if(objects[i].name().equals(name)) return i;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getSizeIndex(int ordinal, int size)
-    {
-    if( ordinal>=0 && ordinal<NUM_OBJECTS )
-      {
-      int[] sizes = objects[ordinal].getSizes();
-      int len = sizes.length;
-
-      for(int i=0; i<len; i++)
-        {
-        if( sizes[i]==size ) return i;
-        }
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int[] retFaceColors(RubikObjectList object)
-    {
-    Field field;
-    int[] faceColors=null;
-
-    try
-      {
-      field = object.mObjectClass.getDeclaredField("FACE_COLORS");
-      field.setAccessible(true);
-      Object obj = field.get(null);
-      faceColors = (int[]) obj;
-      }
-    catch(NoSuchFieldException ex)
-      {
-      android.util.Log.e("RubikObjectList", object.mObjectClass.getSimpleName()+": no such field exception getting field: "+ex.getMessage());
-      }
-    catch(IllegalAccessException ex)
-      {
-      android.util.Log.e("RubikObjectList", object.mObjectClass.getSimpleName()+": illegal access exception getting field: "+ex.getMessage());
-      }
-
-    return faceColors;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikObjectList(int[][] info, Class<? extends RubikObject> object , Movement movement, int column)
-    {
-    mNumSizes = info.length;
-
-    mObjectSizes  = new int[mNumSizes];
-    mMaxLevels    = new int[mNumSizes];
-    mResourceIDs  = new int[mNumSizes];
-    mSmallIconIDs = new int[mNumSizes];
-    mMediumIconIDs= new int[mNumSizes];
-    mBigIconIDs   = new int[mNumSizes];
-    mHugeIconIDs  = new int[mNumSizes];
-
-    for(int i=0; i<mNumSizes; i++)
-      {
-      mObjectSizes[i]  = info[i][0];
-      mMaxLevels[i]    = info[i][1];
-      mResourceIDs[i]  = info[i][2];
-      mSmallIconIDs[i] = info[i][3];
-      mMediumIconIDs[i]= info[i][4];
-      mBigIconIDs[i]   = info[i][5];
-      mHugeIconIDs[i]  = info[i][6];
-      }
-
-    mObjectClass         = object;
-    mObjectMovementClass = movement;
-    mColumn              = column;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getSizes()
-    {
-    return mObjectSizes;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getMaxLevels()
-    {
-    return mMaxLevels;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getIconIDs()
-    {
-    int size = RubikActivity.getDrawableSize();
-
-    switch(size)
-      {
-      case 0 : return mSmallIconIDs;
-      case 1 : return mMediumIconIDs;
-      case 2 : return mBigIconIDs;
-      default: return mHugeIconIDs;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getResourceIDs()
-    {
-    return mResourceIDs;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumVariants()
-    {
-    return mObjectSizes.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public RubikObject create(int size, Static4D quat, int[][] moves, Resources res, int scrWidth)
-    {
-    DistortedTexture texture = new DistortedTexture();
-    DistortedEffects effects = new DistortedEffects();
-    MeshSquare mesh          = new MeshSquare(20,20);   // mesh of the node, not of the cubits
-
-    switch(ordinal())
-      {
-      case 0: return new RubikCube      (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 1: return new RubikPyraminx  (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 2: return new RubikDiamond   (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 3: return new RubikDino6     (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 4: return new RubikDino4     (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 5: return new RubikSkewb     (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 6: return new RubikHelicopter(size, quat, texture, mesh, effects, moves, res, scrWidth);
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Movement getObjectMovementClass()
-    {
-    return mObjectMovementClass;
-    }
-  }
diff --git a/src/main/java/org/distorted/objects/RubikPyraminx.java b/src/main/java/org/distorted/objects/RubikPyraminx.java
deleted file mode 100644
index 96e654b1..00000000
--- a/src/main/java/org/distorted/objects/RubikPyraminx.java
+++ /dev/null
@@ -1,644 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2019 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.objects;
-
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.VertexEffectDeform;
-import org.distorted.library.effect.VertexEffectMove;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.effect.VertexEffectSink;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshPolygon;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.RubikSurfaceView;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikPyraminx extends RubikObject
-{
-  private static final float SQ2 = (float)Math.sqrt(2);
-  private static final float SQ3 = (float)Math.sqrt(3);
-  private static final float SQ6 = (float)Math.sqrt(6);
-
-  static final Static3D[] ROT_AXIS = new Static3D[]
-         {
-           new Static3D(         0,        1,       0 ),
-           new Static3D(         0,  -1.0f/3, 2*SQ2/3 ),
-           new Static3D(-SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 ),
-           new Static3D( SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 )
-         };
-
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(         0,      -1,       0 ),
-           new Static3D(         0,  1.0f/3,-2*SQ2/3 ),
-           new Static3D( SQ2*SQ3/3,  1.0f/3,   SQ2/3 ),
-           new Static3D(-SQ2*SQ3/3,  1.0f/3,   SQ2/3 )
-         };
-
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_GREEN , COLOR_YELLOW,
-           COLOR_BLUE  , COLOR_RED
-         };
-
-  // computed with res/raw/compute_quats.c
-  private static final Static4D[] QUATS = new Static4D[]
-         {
-           new Static4D(  0.0f,   0.0f,   0.0f,  1.0f),
-           new Static4D(  0.0f,  SQ3/2,   0.0f,  0.5f),
-           new Static4D( SQ2/2, -SQ3/6, -SQ6/6,  0.5f),
-           new Static4D(-SQ2/2, -SQ3/6, -SQ6/6,  0.5f),
-           new Static4D(  0.0f, -SQ3/6,  SQ6/3,  0.5f),
-           new Static4D(  0.0f,  SQ3/2,   0.0f, -0.5f),
-           new Static4D( SQ2/2, -SQ3/6, -SQ6/6, -0.5f),
-           new Static4D(-SQ2/2, -SQ3/6, -SQ6/6, -0.5f),
-           new Static4D(  0.0f, -SQ3/6,  SQ6/3, -0.5f),
-           new Static4D( SQ2/2, -SQ3/3,  SQ6/6,  0.0f),
-           new Static4D(  0.0f, -SQ3/3, -SQ6/3,  0.0f),
-           new Static4D(-SQ2/2, -SQ3/3,  SQ6/6,  0.0f)
-         };
-
-  private int[] mRotArray;
-  private static VertexEffectRotate[] ROTATION;
-
-  private static MeshBase mMesh =null;
-  private static MeshBase[] mMeshRotated = new MeshBase[ROT_AXIS.length];
-
-  static
-    {
-    Static3D center = new Static3D(0,0,0);
-    Static1D angle  = new Static1D(180.0f);
-
-    ROTATION = new VertexEffectRotate[ROT_AXIS.length];
-
-    for(int i = 0; i< ROT_AXIS.length; i++)
-      {
-      ROTATION[i] = new VertexEffectRotate( angle, ROT_AXIS[i], center);
-      mMeshRotated[i] = null;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikPyraminx(int size, Static4D quat, DistortedTexture texture,
-                MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, 30, quat, texture, mesh, effects, moves, RubikObjectList.PYRA, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void emitRow(float x, float y, float z, float dx, float dy, float dz, int n, int rot, Static3D[] array, int index)
-    {
-    for(int i=0; i<n; i++)
-      {
-      mRotArray[i+index] = rot;
-      array[i+index] = new Static3D(x+0.5f,y+SQ2*SQ3/12,z+SQ3/6);
-      x += dx;
-      y += dy;
-      z += dz;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int emitLowermost(float x, float y, float z, int n, Static3D[] array)
-    {
-    int added = 0;
-
-    emitRow( x      +0.5f, y+SQ3*SQ2/9, z+ SQ3/18,  1.0f, 0,     0, n-1, 1, array, added);
-    added += (n-1);
-    emitRow( x    +1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9,  0.5f, 0, SQ3/2, n-1, 3, array, added);
-    added += (n-1);
-    emitRow( x+n-1-1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9, -0.5f, 0, SQ3/2, n-1, 2, array, added);
-    added += (n-1);
-
-    for(int i=n; i>=1; i--)
-      {
-      emitRow(x     , y, z      , 1,0,0, i  , -1, array, added);
-      added += i;
-      emitRow(x+0.5f, y, z+SQ3/6, 1,0,0, i-1,  0, array, added);
-      added += (i-1);
-      x += 0.5f;
-      y += 0.0f;
-      z += SQ3/2;
-      }
-
-    return added;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int emitUpper(float x, float y, float z, int n, Static3D[] array, int index)
-    {
-    if( n>1 )
-      {
-      emitRow( x           , y          , z        ,  1.0f, 0,     0, n-1, -1, array, index);
-      index += (n-1);
-      emitRow( x+0.5f      , y+SQ3*SQ2/9, z+SQ3/18 ,  1.0f, 0,     0, n-1,  1, array, index);
-      index += (n-1);
-      emitRow( x+0.5f      , y          , z+SQ3/2  ,  0.5f, 0, SQ3/2, n-1, -1, array, index);
-      index += (n-1);
-      emitRow( x    +1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9,  0.5f, 0, SQ3/2, n-1,  3, array, index);
-      index += (n-1);
-      emitRow( x+n-1       , y          , z        , -0.5f, 0, SQ3/2, n-1, -1, array, index);
-      index += (n-1);
-      emitRow( x+n-1-1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9, -0.5f, 0, SQ3/2, n-1,  2, array, index);
-      index += (n-1);
-      }
-    else
-      {
-      mRotArray[index] = -1;
-      array[index] = new Static3D(x+0.5f,y+SQ2*SQ3/12,z+SQ3/6);
-      index++;
-      }
-
-    return index;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// size^2 + 3*(size-1) in the lowermost layer, then 6*(size-2) in the next, 6*(size-3) in the next,
-// ... 6 in the forelast, 1 in the last = 4size^2 - 6size +4 (if size>1)
-
-  Static3D[] getCubitPositions(int size)
-    {
-    int numCubits = size>1 ? 4*size*size - 6*size +4 : 1;
-    Static3D[] tmp = new Static3D[numCubits];
-    mRotArray = new int[numCubits];
-
-    int currentIndex = emitLowermost( -0.5f*size, -(SQ2*SQ3/12)*size, -(SQ3/6)*size, size, tmp);
-
-    for(int i=size-1; i>=1; i--)
-      {
-      currentIndex = emitUpper( -0.5f*i, ((SQ2*SQ3)/12)*(3*size-4*i), -(SQ3/6)*i, i, tmp, currentIndex);
-      }
-
-    return tmp;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static4D[] getQuats()
-    {
-    return QUATS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumStickerTypes()
-    {
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getBasicStep()
-    {
-    return SQ6/3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumCubitFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.82f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    boolean belongs = isOnFace(cubit, cubitface, 0 );
-    return belongs ? cubitface : NUM_FACES;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private MeshBase createStaticMesh(int cubit)
-    {
-    final float SQ2 = (float)Math.sqrt(2);
-    final float SQ3 = (float)Math.sqrt(3);
-    final float angleFaces = (float)((180/Math.PI)*(2*Math.asin(SQ3/3))); // angle between two faces of a tetrahedron
-    final int MESHES=4;
-
-    int size = getSize();
-    int association = 1;
-
-    float D = 0.0005f;
-    float E = SQ3/2 - 3*D*SQ2;
-    float F = 0.5f - D*SQ2*SQ3;
-    float[] bands;
-    int extraI, extraV;
-
-    float[] vertices = { -F,-E/3, +F,-E/3, 0.0f,2*E/3};
-
-    switch(size)
-      {
-      case 3 : bands = new float[] { 1.0f    ,-D,
-                                     1.0f  -D,-D*0.80f,
-                                     1.0f-2*D,-D*0.65f,
-                                     1.0f-4*D,+D*0.10f,
-                                     0.50f, 0.035f,
-                                     0.0f, 0.040f };
-                      extraI = 2;
-                      extraV = 2;
-                      break;
-      case 4 : bands = new float[] { 1.0f    ,-D,
-                                     1.0f-D*1.2f,-D*0.70f,
-                                     1.0f-3*D, -D*0.15f,
-                                     0.50f, 0.035f,
-                                     0.0f, 0.040f };
-                      extraI = 2;
-                      extraV = 2;
-                      break;
-      default: bands = new float[] { 1.0f    ,-D,
-                                     1.0f-D*1.2f,-D*0.70f,
-                                     1.0f-3*D, -D*0.15f,
-                                     0.50f, 0.035f,
-                                     0.0f, 0.040f };
-                      extraI = 2;
-                      extraV = 1;
-                      break;
-      }
-
-    MeshBase[] meshes = new MeshPolygon[MESHES];
-    meshes[0] = new MeshPolygon(vertices, bands, extraI,extraV);
-    meshes[0].setEffectAssociation(0,association,0);
-
-    for(int i=1; i<MESHES; i++)
-      {
-      association <<= 1;
-      meshes[i] = meshes[0].copy(true);
-      meshes[i].setEffectAssociation(0,association,0);
-      }
-
-    MeshBase result = new MeshJoined(meshes);
-
-    Static3D a0 = new Static3D(         0,        1,       0 );
-    Static3D a1 = new Static3D(         0,  -1.0f/3, 2*SQ2/3 );
-    Static3D a2 = new Static3D(-SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 );
-    Static3D a3 = new Static3D( SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 );
-
-    float tetraHeight = SQ2*SQ3/3;
-    float d1 = (0.75f-2*SQ2*D)*tetraHeight;
-    float d2 =-0.06f*tetraHeight;
-    float d3 = 0.05f*tetraHeight;
-    float d4 = 0.70f*tetraHeight;
-    float d5 = 1.2f;
-
-    Static3D dCen0 = new Static3D( d1*a0.get0(), d1*a0.get1(), d1*a0.get2() );
-    Static3D dCen1 = new Static3D( d1*a1.get0(), d1*a1.get1(), d1*a1.get2() );
-    Static3D dCen2 = new Static3D( d1*a2.get0(), d1*a2.get1(), d1*a2.get2() );
-    Static3D dCen3 = new Static3D( d1*a3.get0(), d1*a3.get1(), d1*a3.get2() );
-
-    Static3D dVec0 = new Static3D( d2*a0.get0(), d2*a0.get1(), d2*a0.get2() );
-    Static3D dVec1 = new Static3D( d2*a1.get0(), d2*a1.get1(), d2*a1.get2() );
-    Static3D dVec2 = new Static3D( d2*a2.get0(), d2*a2.get1(), d2*a2.get2() );
-    Static3D dVec3 = new Static3D( d2*a3.get0(), d2*a3.get1(), d2*a3.get2() );
-
-    Static4D dReg  = new Static4D(0,0,0,d3);
-    Static1D dRad  = new Static1D(1);
-    Static3D center= new Static3D(0,0,0);
-    Static4D sReg  = new Static4D(0,0,0,d4);
-    Static1D sink  = new Static1D(d5);
-
-    Static1D angle  = new Static1D(angleFaces);
-    Static3D axis1  = new Static3D(  -1, 0,      0);
-    Static3D axis2  = new Static3D(0.5f, 0, -SQ3/2);
-    Static3D axis3  = new Static3D(0.5f, 0, +SQ3/2);
-    Static3D center1= new Static3D(0,-SQ3*SQ2/12,-SQ3/6);
-    Static3D center2= new Static3D(0,-SQ3*SQ2/12,+SQ3/3);
-
-    VertexEffectRotate  effect1 = new VertexEffectRotate( new Static1D(90), new Static3D(1,0,0), center );
-    VertexEffectMove    effect2 = new VertexEffectMove  ( new Static3D(0,-SQ3*SQ2/12,0) );
-    VertexEffectRotate  effect3 = new VertexEffectRotate( new Static1D(180), new Static3D(0,0,1), center1 );
-    VertexEffectRotate  effect4 = new VertexEffectRotate( angle, axis1, center1 );
-    VertexEffectRotate  effect5 = new VertexEffectRotate( angle, axis2, center2 );
-    VertexEffectRotate  effect6 = new VertexEffectRotate( angle, axis3, center2 );
-
-    VertexEffectDeform  effect7 = new VertexEffectDeform(dVec0, dRad, dCen0, dReg);
-    VertexEffectDeform  effect8 = new VertexEffectDeform(dVec1, dRad, dCen1, dReg);
-    VertexEffectDeform  effect9 = new VertexEffectDeform(dVec2, dRad, dCen2, dReg);
-    VertexEffectDeform  effect10= new VertexEffectDeform(dVec3, dRad, dCen3, dReg);
-
-    VertexEffectSink effect11= new VertexEffectSink(sink,center, sReg);
-
-    effect3.setMeshAssociation(14,-1);  // apply to mesh[1], [2] and [3]
-    effect4.setMeshAssociation( 2,-1);  // apply only to mesh[1]
-    effect5.setMeshAssociation( 4,-1);  // apply only to mesh[2]
-    effect6.setMeshAssociation( 8,-1);  // apply only to mesh[3]
-
-    result.apply(effect1);
-    result.apply(effect2);
-    result.apply(effect3);
-    result.apply(effect4);
-    result.apply(effect5);
-    result.apply(effect6);
-
-    result.apply(effect7);
-    result.apply(effect8);
-    result.apply(effect9);
-    result.apply(effect10);
-
-    result.apply(effect11);
-
-    if( mRotArray[cubit]>=0 )
-      {
-      result.apply( ROTATION[mRotArray[cubit]] );
-      }
-
-    result.mergeEffComponents();
-
-    return result;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int cubit)
-    {
-    int kind = mRotArray[cubit];
-
-    if( kind>=0 )
-      {
-      if( mMeshRotated[kind]==null ) mMeshRotated[kind] = createStaticMesh(cubit);
-      return mMeshRotated[kind].copy(true);
-      }
-    else
-      {
-      if( mMesh==null ) mMesh = createStaticMesh(cubit);
-      return mMesh.copy(true);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
-    {
-    float STROKE = 0.044f*side;
-    float OFF = STROKE/2 -1;
-    float OFF2 = 0.5f*side + OFF;
-    float HEIGHT = side - OFF;
-    float RADIUS = side/12.0f;
-    float ARC1_H = 0.2f*side;
-    float ARC1_W = side*0.5f;
-    float ARC2_W = 0.153f*side;
-    float ARC2_H = 0.905f*side;
-    float ARC3_W = side-ARC2_W;
-
-    float M = SQ3/2;
-    float D = (M/2 - 0.51f)*side;
-
-    paint.setAntiAlias(true);
-    paint.setStrokeWidth(STROKE);
-    paint.setColor(FACE_COLORS[face]);
-    paint.setStyle(Paint.Style.FILL);
-
-    canvas.drawRect(left,top,left+side,top+side,paint);
-
-    paint.setColor(INTERIOR_COLOR);
-    paint.setStyle(Paint.Style.STROKE);
-
-    canvas.drawLine(           left, M*HEIGHT+D,  side       +left, M*HEIGHT+D, paint);
-    canvas.drawLine(      OFF +left, M*side  +D,       OFF2  +left,          D, paint);
-    canvas.drawLine((side-OFF)+left, M*side  +D, (side-OFF2) +left,          D, paint);
-
-    canvas.drawArc( ARC1_W-RADIUS+left, M*(ARC1_H-RADIUS)+D, ARC1_W+RADIUS+left, M*(ARC1_H+RADIUS)+D, 225, 90, false, paint);
-    canvas.drawArc( ARC2_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC2_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 105, 90, false, paint);
-    canvas.drawArc( ARC3_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC3_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 345, 90, false, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// SQ6/3 = height of the tetrahedron
-
-  float returnMultiplier()
-    {
-    return getSize()/(SQ6/3);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getRowChances()
-    {
-    int size = getSize();
-    int total = size*(size+1)/2;
-    float running=0.0f;
-    float[] chances = new float[size];
-
-    for(int i=0; i<size; i++)
-      {
-      running += (size-i);
-      chances[i] = running / total;
-      }
-
-    return chances;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public Static3D[] getRotationAxis()
-    {
-    return ROT_AXIS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getBasicAngle()
-    {
-    return 3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeRowFromOffset(float offset)
-    {
-    return (int)(getSize()*offset/(SQ6/3));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(float offset)
-    {
-    int size = getSize();
-    int row  = (int)(size*offset/(SQ3/2));
-
-    return ((float)size)/(size-row);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
-    {
-    int numAxis = ROTATION_AXIS.length;
-
-    if( oldRotAxis == START_AXIS )
-      {
-      return rnd.nextInt(numAxis);
-      }
-    else
-      {
-      int newVector = rnd.nextInt(numAxis-1);
-      return (newVector>=oldRotAxis ? newVector+1 : newVector);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    float rowFloat = rnd.nextFloat();
-
-    for(int row=0; row<mRowChances.length; row++)
-      {
-      if( rowFloat<=mRowChances[row] ) return row;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean isSolved()
-    {
-    int index = CUBITS[0].mQuatIndex;
-
-    for(int i=1; i<NUM_CUBITS; i++)
-      {
-      if( !thereIsNoVisibleDifference(CUBITS[i], index) ) return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return if the Cubit, when rotated with its own mQuatScramble, would have looked any different
-// then if it were rotated by quaternion 'quat'.
-// No it is not so simple as the quats need to be the same - imagine a 4x4x4 cube where the two
-// middle squares get interchanged. No visible difference!
-//
-// So: this is true iff the cubit
-// a) is a corner or edge and the quaternions are the same
-// b) is inside one of the faces and after rotations by both quats it ends up on the same face.
-
-  private boolean thereIsNoVisibleDifference(Cubit cubit, int quatIndex)
-    {
-    if ( cubit.mQuatIndex == quatIndex ) return true;
-
-    int belongsToHowManyFaces = 0;
-    int size = getSize()-1;
-    float row;
-    final float MAX_ERROR = 0.01f;
-
-    for(int i=0; i<NUM_AXIS; i++)
-      {
-      row = cubit.mRotationRow[i];
-      if( (row     <MAX_ERROR && row     >-MAX_ERROR) ||
-          (row-size<MAX_ERROR && row-size>-MAX_ERROR)  ) belongsToHowManyFaces++;
-      }
-
-    switch(belongsToHowManyFaces)
-      {
-      case 0 : return true ;  // 'inside' cubit that does not lie on any face
-      case 1 :                // cubit that lies inside one of the faces
-               Static3D orig = cubit.getOrigPosition();
-               Static4D quat1 = QUATS[quatIndex];
-               Static4D quat2 = QUATS[cubit.mQuatIndex];
-
-               Static4D cubitCenter = new Static4D( orig.get0(), orig.get1(), orig.get2(), 0);
-               Static4D rotated1 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat1 );
-               Static4D rotated2 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat2 );
-
-               float row1, row2, row3, row4;
-               float ax,ay,az;
-               Static3D axis;
-               float x1 = rotated1.get0();
-               float y1 = rotated1.get1();
-               float z1 = rotated1.get2();
-               float x2 = rotated2.get0();
-               float y2 = rotated2.get1();
-               float z2 = rotated2.get2();
-
-               for(int i=0; i<NUM_AXIS; i++)
-                 {
-                 axis = ROTATION_AXIS[i];
-                 ax = axis.get0();
-                 ay = axis.get1();
-                 az = axis.get2();
-
-                 row1 = ((x1*ax + y1*ay + z1*az) - mStart) / mStep;
-                 row2 = ((x2*ax + y2*ay + z2*az) - mStart) / mStep;
-                 row3 = row1 - size;
-                 row4 = row2 - size;
-
-                 if( (row1<MAX_ERROR && row1>-MAX_ERROR && row2<MAX_ERROR && row2>-MAX_ERROR) ||
-                     (row3<MAX_ERROR && row3>-MAX_ERROR && row4<MAX_ERROR && row4>-MAX_ERROR)  )
-                   {
-                   return true;
-                   }
-                 }
-               return false;
-
-      default: return false;  // edge or corner
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// only needed for solvers - there are no Pyraminx solvers ATM)
-
-  public String retObjectString()
-    {
-    return "";
-    }
-}
diff --git a/src/main/java/org/distorted/objects/RubikSkewb.java b/src/main/java/org/distorted/objects/RubikSkewb.java
deleted file mode 100644
index 7b0e07de..00000000
--- a/src/main/java/org/distorted/objects/RubikSkewb.java
+++ /dev/null
@@ -1,709 +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.objects;
-
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.effect.VertexEffectDeform;
-import org.distorted.library.effect.VertexEffectMove;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.effect.VertexEffectScale;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshPolygon;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.mesh.MeshTriangle;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.RubikSurfaceView;
-
-import java.util.Random;
-
-import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikSkewb extends RubikObject
-{
-  private static final float SQ2 = (float)Math.sqrt(2);
-  private static final float SQ3 = (float)Math.sqrt(3);
-
-  private static final int FACES_PER_CUBIT =6;
-
-  // the four rotation axis of a RubikSkewb. Must be normalized.
-  static final Static3D[] ROT_AXIS = new Static3D[]
-         {
-           new Static3D(+SQ3/3,+SQ3/3,+SQ3/3),
-           new Static3D(+SQ3/3,+SQ3/3,-SQ3/3),
-           new Static3D(+SQ3/3,-SQ3/3,+SQ3/3),
-           new Static3D(+SQ3/3,-SQ3/3,-SQ3/3)
-         };
-
-  // the six axis that determine the faces
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(1,0,0), new Static3D(-1,0,0),
-           new Static3D(0,1,0), new Static3D(0,-1,0),
-           new Static3D(0,0,1), new Static3D(0,0,-1)
-         };
-
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_YELLOW, COLOR_WHITE,
-           COLOR_BLUE  , COLOR_GREEN,
-           COLOR_RED   , COLOR_BROWN
-         };
-
-  // All legal rotation quats of a RubikSkewb
-  private static final Static4D[] QUATS = new Static4D[]
-         {
-           new Static4D(  0.0f,  0.0f,  0.0f,  1.0f ),
-           new Static4D(  1.0f,  0.0f,  0.0f,  0.0f ),
-           new Static4D(  0.0f,  1.0f,  0.0f,  0.0f ),
-           new Static4D(  0.0f,  0.0f,  1.0f,  0.0f ),
-
-           new Static4D(  0.5f,  0.5f,  0.5f,  0.5f ),
-           new Static4D(  0.5f,  0.5f,  0.5f, -0.5f ),
-           new Static4D(  0.5f,  0.5f, -0.5f,  0.5f ),
-           new Static4D(  0.5f,  0.5f, -0.5f, -0.5f ),
-           new Static4D(  0.5f, -0.5f,  0.5f,  0.5f ),
-           new Static4D(  0.5f, -0.5f,  0.5f, -0.5f ),
-           new Static4D(  0.5f, -0.5f, -0.5f,  0.5f ),
-           new Static4D(  0.5f, -0.5f, -0.5f, -0.5f )
-         };
-
-  private static final float DIST_CORNER = 0.50f;
-  private static final float DIST_CENTER = 0.49f;
-
-  // centers of the 8 corners + 6 sides ( i.e. of the all 14 cubits)
-  private static final Static3D[] CENTERS = new Static3D[]
-         {
-           new Static3D( DIST_CORNER, DIST_CORNER, DIST_CORNER ),
-           new Static3D( DIST_CORNER, DIST_CORNER,-DIST_CORNER ),
-           new Static3D( DIST_CORNER,-DIST_CORNER, DIST_CORNER ),
-           new Static3D( DIST_CORNER,-DIST_CORNER,-DIST_CORNER ),
-           new Static3D(-DIST_CORNER, DIST_CORNER, DIST_CORNER ),
-           new Static3D(-DIST_CORNER, DIST_CORNER,-DIST_CORNER ),
-           new Static3D(-DIST_CORNER,-DIST_CORNER, DIST_CORNER ),
-           new Static3D(-DIST_CORNER,-DIST_CORNER,-DIST_CORNER ),
-
-           new Static3D( DIST_CENTER,        0.0f,        0.0f ),
-           new Static3D(-DIST_CENTER,        0.0f,        0.0f ),
-           new Static3D(        0.0f, DIST_CENTER,        0.0f ),
-           new Static3D(        0.0f,-DIST_CENTER,        0.0f ),
-           new Static3D(        0.0f,        0.0f, DIST_CENTER ),
-           new Static3D(        0.0f,        0.0f,-DIST_CENTER ),
-         };
-
-  // Colors of the faces of cubits. Each cubit, even the face pyramid, has 6 faces
-  // (the face has one extra 'fake' face so that everything would have the same number)
-  private static final int[][] mFaceMap = new int[][]
-         {
-           { 4,2,0, 12,12,12 },
-           { 2,5,0, 12,12,12 },
-           { 3,4,0, 12,12,12 },
-           { 5,3,0, 12,12,12 },
-           { 1,2,4, 12,12,12 },
-           { 5,2,1, 12,12,12 },
-           { 4,3,1, 12,12,12 },
-           { 1,3,5, 12,12,12 },
-
-           { 6 , 12,12,12,12,12 },
-           { 7 , 12,12,12,12,12 },
-           { 8 , 12,12,12,12,12 },
-           { 9 , 12,12,12,12,12 },
-           { 10, 12,12,12,12,12 },
-           { 11, 12,12,12,12,12 },
-         };
-
-  private static MeshBase mCornerMesh, mFaceMesh;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  RubikSkewb(int size, Static4D quat, DistortedTexture texture,
-             MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
-    {
-    super(size, 60, quat, texture, mesh, effects, moves, RubikObjectList.SKEW, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createCornerMesh()
-    {
-    float D = 0.02f;
-    float E = 0.5f;
-    float F = SQ2/2;
-
-    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
-
-    float[] bands0 = { 1.0f    , 0,
-                       1.0f-2*D, D*0.25f,
-                       1.0f-4*D, D*0.35f,
-                       1.0f-8*D, D*0.6f,
-                       0.60f   , D*1.0f,
-                       0.30f   , D*1.375f,
-                       0.0f    , D*1.4f };
-
-    MeshBase[] meshes = new MeshBase[FACES_PER_CUBIT];
-
-    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
-    meshes[0].setEffectAssociation(0,1,0);
-    meshes[1] = meshes[0].copy(true);
-    meshes[1].setEffectAssociation(0,2,0);
-    meshes[2] = meshes[0].copy(true);
-    meshes[2].setEffectAssociation(0,4,0);
-
-    float[] vertices1 = { 0,0, F,0, F/2,(SQ3/2)*F };
-    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
-
-    meshes[3] = new MeshPolygon(vertices1,bands1,1,5);
-    meshes[3].setEffectAssociation(0,8,0);
-    meshes[4] = meshes[3].copy(true);
-    meshes[4].setEffectAssociation(0,16,0);
-    meshes[5] = meshes[3].copy(true);
-    meshes[5].setEffectAssociation(0,32,0);
-
-    mCornerMesh = new MeshJoined(meshes);
-
-    Static3D axisX  = new Static3D(1,0,0);
-    Static3D axisY  = new Static3D(0,1,0);
-    Static3D axis0  = new Static3D(-SQ2/2,0,SQ2/2);
-    Static3D axis1  = new Static3D(+SQ3/3,+SQ3/3,+SQ3/3);
-    Static1D angle1 = new Static1D(+90);
-    Static1D angle2 = new Static1D(-90);
-    Static1D angle3 = new Static1D(-15);
-    Static1D angle4 = new Static1D((float)((180.0f/Math.PI)*Math.acos(SQ3/3)));
-    Static1D angle5 = new Static1D(120);
-    Static1D angle6 = new Static1D(240);
-    Static3D center1= new Static3D(0,0,0);
-    Static3D center2= new Static3D(-0.5f,-0.5f,-0.5f);
-    Static3D move1  = new Static3D(-E/4,-E/4,0);
-    Static3D move2  = new Static3D(-0.5f,-0.5f,-0.5f);
-
-    float d0 =-0.04f;
-    float d1 = 0.04f;
-    float r0 = 0.15f;
-    float r1 = 0.10f;
-
-    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
-    Static3D vec1   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(-SQ3/3));
-    Static3D vec2   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(-SQ3/3));
-    Static3D vec3   = new Static3D(d1*(-SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
-
-    Static1D radius = new Static1D(0.5f);
-
-    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
-    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
-    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
-    Static3D cent3  = new Static3D( 0.0f, 0.0f,-0.5f);
-
-    Static4D reg0   = new Static4D(0,0,0,r0);
-    Static4D reg1   = new Static4D(0,0,0,r1);
-
-    VertexEffectMove   effect0 = new VertexEffectMove(move1);
-    VertexEffectScale  effect1 = new VertexEffectScale(new Static3D(1,1,-1));
-    VertexEffectRotate effect2 = new VertexEffectRotate(angle1,axisX,center1);
-    VertexEffectRotate effect3 = new VertexEffectRotate(angle2,axisY,center1);
-    VertexEffectMove   effect4 = new VertexEffectMove(move2);
-    VertexEffectRotate effect5 = new VertexEffectRotate(angle1,axisX,center2);
-    VertexEffectRotate effect6 = new VertexEffectRotate(angle3,axisY,center2);
-    VertexEffectRotate effect7 = new VertexEffectRotate(angle4,axis0,center2);
-    VertexEffectRotate effect8 = new VertexEffectRotate(angle5,axis1,center2);
-    VertexEffectRotate effect9 = new VertexEffectRotate(angle6,axis1,center2);
-
-    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
-    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
-    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
-    VertexEffectDeform effect13= new VertexEffectDeform(vec3,radius,cent3,reg1);
-
-    effect0.setMeshAssociation( 7,-1);  // meshes 0,1,2
-    effect1.setMeshAssociation( 6,-1);  // meshes 1,2
-    effect2.setMeshAssociation( 2,-1);  // mesh 1
-    effect3.setMeshAssociation( 4,-1);  // mesh 2
-    effect4.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect5.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect6.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect7.setMeshAssociation(56,-1);  // meshes 3,4,5
-    effect8.setMeshAssociation(16,-1);  // mesh 4
-    effect9.setMeshAssociation(32,-1);  // mesh 5
-
-    effect10.setMeshAssociation(63,-1); // all meshes
-    effect11.setMeshAssociation(63,-1); // all meshes
-    effect12.setMeshAssociation(63,-1); // all meshes
-    effect13.setMeshAssociation(63,-1); // all meshes
-
-    mCornerMesh.apply(effect0);
-    mCornerMesh.apply(effect1);
-    mCornerMesh.apply(effect2);
-    mCornerMesh.apply(effect3);
-    mCornerMesh.apply(effect4);
-    mCornerMesh.apply(effect5);
-    mCornerMesh.apply(effect6);
-    mCornerMesh.apply(effect7);
-    mCornerMesh.apply(effect8);
-    mCornerMesh.apply(effect9);
-
-    mCornerMesh.apply(effect10);
-    mCornerMesh.apply(effect11);
-    mCornerMesh.apply(effect12);
-    mCornerMesh.apply(effect13);
-
-    mCornerMesh.mergeEffComponents();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createFaceMesh()
-    {
-    int association = 1;
-
-    float D = 0.03f;
-    float E = SQ2/4;
-    float[] vertices0 = { -E,-E, +E,-E, +E,+E, -E,+E };
-
-    float[] bands0 = { 1.0f    , 0,
-                       1.0f-D/2, D*0.30f,
-                       1.0f- D , D*0.50f,
-                       1.0f-2*D, D*0.80f,
-                       0.60f   , D*1.40f,
-                       0.30f   , D*1.60f,
-                       0.0f    , D*1.70f };
-
-    MeshBase[] meshes = new MeshBase[FACES_PER_CUBIT];
-    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
-    meshes[0].setEffectAssociation(0,association,0);
-
-    association <<= 1;
-
-    float[] vertices1 = { -E,-SQ3*E, +E,-SQ3*E, 0,0 };
-    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
-
-    meshes[1] = new MeshPolygon(vertices1,bands1,0,0);
-    meshes[1].setEffectAssociation(0,association,0);
-
-    for(int i=2; i<FACES_PER_CUBIT-1; i++)
-      {
-      association <<= 1;
-      meshes[i] = meshes[1].copy(true);
-      meshes[i].setEffectAssociation(0,association,0);
-      }
-
-    association <<= 1;
-    meshes[FACES_PER_CUBIT-1] = new MeshTriangle(1);                  // empty triangle so that
-    meshes[FACES_PER_CUBIT-1].setEffectAssociation(0,association,0);  // all cubits have 6 faces
-
-    mFaceMesh = new MeshJoined(meshes);
-
-    Static3D center = new Static3D(0,0,0);
-    Static3D axis1   = new Static3D(1,0,0);
-    Static3D axis2   = new Static3D(0,0,1);
-    float angle = -(float)((180.0f/Math.PI)*Math.acos(SQ3/3));
-
-    float f = 0.05f;
-    float r = 0.10f;
-    float d = 0.5f;
-    float e = +D*0.6f;
-    Static3D vector0 = new Static3D(-f, 0, 0);
-    Static3D vector1 = new Static3D( 0,+f, 0);
-    Static3D vector2 = new Static3D(+f, 0, 0);
-    Static3D vector3 = new Static3D( 0,-f, 0);
-    Static1D radius  = new Static1D(1.0f);
-    Static4D region  = new Static4D(0,0,0,r);
-    Static3D center0 = new Static3D(+d, 0, e);
-    Static3D center1 = new Static3D( 0,-d, e);
-    Static3D center2 = new Static3D(-d, 0, e);
-    Static3D center3 = new Static3D( 0,+d, e);
-
-    VertexEffectRotate effect0 = new VertexEffectRotate( new Static1D(angle), axis1, center);
-    VertexEffectRotate effect1 = new VertexEffectRotate( new Static1D(  135), axis2, center);
-    VertexEffectRotate effect2 = new VertexEffectRotate( new Static1D(   45), axis2, center);
-    VertexEffectRotate effect3 = new VertexEffectRotate( new Static1D(  -45), axis2, center);
-    VertexEffectRotate effect4 = new VertexEffectRotate( new Static1D( -135), axis2, center);
-    VertexEffectMove   effect5 = new VertexEffectMove( new Static3D(0,0,-0.5f) );
-    VertexEffectDeform effect6 = new VertexEffectDeform(vector0,radius,center0,region);
-    VertexEffectDeform effect7 = new VertexEffectDeform(vector1,radius,center1,region);
-    VertexEffectDeform effect8 = new VertexEffectDeform(vector2,radius,center2,region);
-    VertexEffectDeform effect9 = new VertexEffectDeform(vector3,radius,center3,region);
-    VertexEffectScale  effect10= new VertexEffectScale(0.01f);
-
-    effect0.setMeshAssociation(30,-1);  // meshes 1,2,3,4
-    effect1.setMeshAssociation( 2,-1);  // mesh 1
-    effect2.setMeshAssociation( 5,-1);  // meshes 0,2
-    effect3.setMeshAssociation( 8,-1);  // mesh 3
-    effect4.setMeshAssociation(16,-1);  // mesh 4
-    effect5.setMeshAssociation(30,-1);  // meshes 1,2,3,4
-    effect6.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
-    effect7.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
-    effect8.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
-    effect9.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
-    effect10.setMeshAssociation(32,-1); // mesh 5
-
-    mFaceMesh.apply(effect0);
-    mFaceMesh.apply(effect1);
-    mFaceMesh.apply(effect2);
-    mFaceMesh.apply(effect3);
-    mFaceMesh.apply(effect4);
-    mFaceMesh.apply(effect5);
-    mFaceMesh.apply(effect6);
-    mFaceMesh.apply(effect7);
-    mFaceMesh.apply(effect8);
-    mFaceMesh.apply(effect9);
-    mFaceMesh.apply(effect10);
-
-    mFaceMesh.mergeEffComponents();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static4D[] getQuats()
-    {
-    return QUATS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaces()
-    {
-    return FACE_COLORS.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Each face has two types of a texture: the central square and the triangle in the corner.
-
-  int getNumStickerTypes()
-    {
-    return 2;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getBasicStep()
-    {
-    return SQ3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumCubitFaces()
-    {
-    return FACES_PER_CUBIT;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Static3D[] getCubitPositions(int size)
-    {
-    return CENTERS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Static4D getQuat(int cubit)
-    {
-    switch(cubit)
-      {
-      case  0: return QUATS[0];                          //  unit quat
-      case  1: return new Static4D( SQ2/2,0,0,SQ2/2);    //  90 along X
-      case  2: return new Static4D(-SQ2/2,0,0,SQ2/2);    // -90 along X
-      case  3: return QUATS[1];                          // 180 along X
-      case  4: return new Static4D(0, SQ2/2,0,SQ2/2);    //  90 along Y
-      case  5: return QUATS[2];                          // 180 along Y
-      case  6: return QUATS[3];                          // 180 along Z
-      case  7: return new Static4D(SQ2/2,0,-SQ2/2,0);    // 180 along (SQ2/2,0,-SQ2/2)
-      case  8: return new Static4D(0,-SQ2/2,0,SQ2/2);    // -90 along Y
-      case  9: return new Static4D(0, SQ2/2,0,SQ2/2);    //  90 along Y
-      case 10: return new Static4D( SQ2/2,0,0,SQ2/2);    //  90 along X
-      case 11: return new Static4D(-SQ2/2,0,0,SQ2/2);    // -90 along X
-      case 12: return QUATS[0];                          //  unit quaternion
-      case 13: return QUATS[1];                          // 180 along X
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  MeshBase createCubitMesh(int cubit)
-    {
-    MeshBase mesh;
-
-    if( cubit<8 )
-      {
-      if( mCornerMesh==null ) createCornerMesh();
-      mesh = mCornerMesh.copy(true);
-      }
-    else
-      {
-      if( mFaceMesh==null ) createFaceMesh();
-      mesh = mFaceMesh.copy(true);
-      }
-
-    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( getQuat(cubit), new Static3D(0,0,0) );
-    mesh.apply(quat,0xffffffff,0);
-
-    return mesh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFaceColor(int cubit, int cubitface, int size)
-    {
-    return mFaceMap[cubit][cubitface];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
-    {
-    int COLORS = FACE_COLORS.length;
-
-    if( face<COLORS )
-      {
-      float STROKE = 0.035f*side;
-      float L= left+0.125f*side;
-      float H= 0.375f*side;
-      float LEN = 0.5f*side;
-
-      paint.setAntiAlias(true);
-      paint.setStrokeWidth(STROKE);
-      paint.setColor(FACE_COLORS[face]);
-      paint.setStyle(Paint.Style.FILL);
-
-      canvas.drawRect(left,top,left+side,top+side,paint);
-
-      paint.setColor(INTERIOR_COLOR);
-      paint.setStyle(Paint.Style.STROKE);
-
-      canvas.drawLine( L    , H,  L+LEN, H    , paint);
-      canvas.drawLine( L    , H,  L+LEN, H+LEN, paint);
-      canvas.drawLine( L+LEN, H,  L+LEN, H+LEN, paint);
-
-      float S1 = 0.125f*side;
-      float S2 = 0.070f*side;
-      float X  = 0.7f*S2;
-
-      float LA = left+0.625f*side;
-      float RA = left+0.125f*side;
-      float TA = 0.375f*side;
-      float BA = 0.875f*side;
-
-      canvas.drawArc( LA-S1, TA     , LA     , TA+S1, 270, 90, false, paint);
-      canvas.drawArc( RA+X , TA     , RA+X+S2, TA+S2, 135,135, false, paint);
-      canvas.drawArc( LA-S2, BA-X-S2, LA     , BA-X ,   0,135, false, paint);
-      }
-    else
-      {
-      final float R = (SQ2/2)*side*0.10f;
-      final float M = side*(0.5f-SQ2/4+0.018f);
-
-      paint.setColor(FACE_COLORS[face-COLORS]);
-      paint.setStyle(Paint.Style.FILL);
-      canvas.drawRoundRect( left+M, top+M, left+side-M, top+side-M, R, R, paint);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return 2.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getRowChances()
-    {
-    float[] chances = new float[2];
-
-    chances[0] = 0.5f;
-    chances[1] = 1.0f;
-
-    return chances;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public Static3D[] getRotationAxis()
-    {
-    return ROT_AXIS;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getBasicAngle()
-    {
-    return 3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int computeRowFromOffset(float offset)
-    {
-    return offset<0.25f ? 0:1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(float offset)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
-    {
-    int numAxis = ROTATION_AXIS.length;
-
-    if( oldRotAxis == START_AXIS )
-      {
-      return rnd.nextInt(numAxis);
-      }
-    else
-      {
-      int newVector = rnd.nextInt(numAxis-1);
-      return (newVector>=oldRotAxis ? newVector+1 : newVector);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
-    {
-    float rowFloat = rnd.nextFloat();
-
-    for(int row=0; row<mRowChances.length; row++)
-      {
-      if( rowFloat<=mRowChances[row] ) return row;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// remember about the double cover or unit quaternions!
-
-  private int mulQuat(int q1, int q2)
-    {
-    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
-
-    float rX = result.get0();
-    float rY = result.get1();
-    float rZ = result.get2();
-    float rW = result.get3();
-
-    final float MAX_ERROR = 0.1f;
-    float dX,dY,dZ,dW;
-
-    for(int i=0; i<QUATS.length; i++)
-      {
-      dX = QUATS[i].get0() - rX;
-      dY = QUATS[i].get1() - rY;
-      dZ = QUATS[i].get2() - rZ;
-      dW = QUATS[i].get3() - rW;
-
-      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
-          dY<MAX_ERROR && dY>-MAX_ERROR &&
-          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
-          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
-
-      dX = QUATS[i].get0() + rX;
-      dY = QUATS[i].get1() + rY;
-      dZ = QUATS[i].get2() + rZ;
-      dW = QUATS[i].get3() + rW;
-
-      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
-          dY<MAX_ERROR && dY>-MAX_ERROR &&
-          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
-          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// The Skewb is solved if and only if:
-//
-// 1) all of its corner cubits are rotated with the same quat
-// 2) all its face cubits are rotated with the same quat like the corner ones,
-//    and optionally they also might be upside down.
-//
-// i.e.
-// cubits [ 8] and [ 9] - might be extra QUAT[1]
-// cubits [10] and [11] - might be extra QUAT[2]
-// cubits [12] and [13] - might be extra QUAT[3]
-
-  public boolean isSolved()
-    {
-    int q = CUBITS[0].mQuatIndex;
-
-    if ( CUBITS[1].mQuatIndex == q &&
-         CUBITS[2].mQuatIndex == q &&
-         CUBITS[3].mQuatIndex == q &&
-         CUBITS[4].mQuatIndex == q &&
-         CUBITS[5].mQuatIndex == q &&
-         CUBITS[6].mQuatIndex == q &&
-         CUBITS[7].mQuatIndex == q  )
-      {
-      int q1 = mulQuat(q,1);
-      int q2 = mulQuat(q,2);
-      int q3 = mulQuat(q,3);
-
-      return (CUBITS[ 8].mQuatIndex == q || CUBITS[ 8].mQuatIndex == q1) &&
-             (CUBITS[ 9].mQuatIndex == q || CUBITS[ 9].mQuatIndex == q1) &&
-             (CUBITS[10].mQuatIndex == q || CUBITS[10].mQuatIndex == q2) &&
-             (CUBITS[11].mQuatIndex == q || CUBITS[11].mQuatIndex == q2) &&
-             (CUBITS[12].mQuatIndex == q || CUBITS[12].mQuatIndex == q3) &&
-             (CUBITS[13].mQuatIndex == q || CUBITS[13].mQuatIndex == q3)  ;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// only needed for solvers - there are no Skewb solvers ATM)
-
-  public String retObjectString()
-    {
-    return "";
-    }
-
-}
diff --git a/src/main/java/org/distorted/objects/TwistyCube.java b/src/main/java/org/distorted/objects/TwistyCube.java
new file mode 100644
index 00000000..7782ca77
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyCube.java
@@ -0,0 +1,658 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.effect.VertexEffectMove;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.RubikSurfaceView;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TwistyCube extends TwistyObject
+{
+  static final float SQ2 = (float)Math.sqrt(2);
+
+  // the three rotation axis of a RubikCube. Must be normalized.
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(1,0,0),
+           new Static3D(0,1,0),
+           new Static3D(0,0,1)
+         };
+
+  // the six axis that determine the faces
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(1,0,0), new Static3D(-1,0,0),
+           new Static3D(0,1,0), new Static3D(0,-1,0),
+           new Static3D(0,0,1), new Static3D(0,0,-1)
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_YELLOW, COLOR_WHITE,
+           COLOR_BLUE  , COLOR_GREEN,
+           COLOR_RED   , COLOR_BROWN
+         };
+
+  // All legal rotation quats of a RubikCube of any size.
+  // Here's how to compute this:
+  // 1) compute how many rotations there are (RubikCube of any size = 24)
+  // 2) take the AXIS, angles of rotation (90 in RubikCube's case) compute the basic quaternions
+  // (i.e. rotations of 1 basic angle along each of the axis) and from there start semi-randomly
+  // multiplying them and eventually you'll find all (24) legal rotations.
+  // Example program in C, res/raw/compute_quats.c , is included.
+  private static final Static4D[] QUATS = new Static4D[]
+         {
+         new Static4D(  0.0f,   0.0f,   0.0f,   1.0f),
+         new Static4D(  1.0f,   0.0f,   0.0f,   0.0f),
+         new Static4D(  0.0f,   1.0f,   0.0f,   0.0f),
+         new Static4D(  0.0f,   0.0f,   1.0f,   0.0f),
+
+         new Static4D( SQ2/2,  SQ2/2,  0.0f ,   0.0f),
+         new Static4D( SQ2/2, -SQ2/2,  0.0f ,   0.0f),
+         new Static4D( SQ2/2,   0.0f,  SQ2/2,   0.0f),
+         new Static4D(-SQ2/2,   0.0f,  SQ2/2,   0.0f),
+         new Static4D( SQ2/2,   0.0f,   0.0f,  SQ2/2),
+         new Static4D( SQ2/2,   0.0f,   0.0f, -SQ2/2),
+         new Static4D(  0.0f,  SQ2/2,  SQ2/2,   0.0f),
+         new Static4D(  0.0f,  SQ2/2, -SQ2/2,   0.0f),
+         new Static4D(  0.0f,  SQ2/2,   0.0f,  SQ2/2),
+         new Static4D(  0.0f,  SQ2/2,   0.0f, -SQ2/2),
+         new Static4D(  0.0f,   0.0f,  SQ2/2,  SQ2/2),
+         new Static4D(  0.0f,   0.0f,  SQ2/2, -SQ2/2),
+
+         new Static4D(  0.5f,   0.5f,   0.5f,   0.5f),
+         new Static4D(  0.5f,   0.5f,  -0.5f,   0.5f),
+         new Static4D(  0.5f,   0.5f,  -0.5f,  -0.5f),
+         new Static4D(  0.5f,  -0.5f,   0.5f,  -0.5f),
+         new Static4D( -0.5f,  -0.5f,  -0.5f,   0.5f),
+         new Static4D( -0.5f,   0.5f,  -0.5f,  -0.5f),
+         new Static4D( -0.5f,   0.5f,   0.5f,  -0.5f),
+         new Static4D( -0.5f,   0.5f,   0.5f,   0.5f)
+         };
+
+  private static MeshBase[] mMeshes;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyCube(int size, Static4D quat, DistortedTexture texture,
+             MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, 60, quat, texture, mesh, effects, moves, ObjectList.CUBE, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// paint the square with upper-right corner at (left,top) and side length 'side' with texture
+// for face 'face'.
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    final float R = side*0.10f;
+    final float M = side*0.05f;
+
+    paint.setColor(FACE_COLORS[face]);
+    canvas.drawRoundRect( left+M, top+M, left+side-M, top+side-M, R, R, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static3D[] getCubitPositions(int size)
+    {
+    int numCubits = size>1 ? 6*size*size - 12*size + 8 : 1;
+    Static3D[] tmp = new Static3D[numCubits];
+
+    float diff = 0.5f*(size-1);
+    int currentPosition = 0;
+
+    for(int x = 0; x<size; x++)
+      for(int y = 0; y<size; y++)
+        for(int z = 0; z<size; z++)
+          if( x==0 || x==size-1 || y==0 || y==size-1 || z==0 || z==size-1 )
+            {
+            tmp[currentPosition++] = new Static3D(x-diff,y-diff,z-diff);
+            }
+
+    return tmp;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getBasicStep()
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumStickerTypes()
+    {
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.5f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    boolean belongs = isOnFace(cubit, cubitface/2, cubitface%2==0 ? size-1:0 );
+    return belongs ? cubitface : NUM_FACES;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    int size   = getSize();
+    int ordinal= ObjectList.CUBE.ordinal();
+    int index  = ObjectList.getSizeIndex(ordinal,size);
+    float[] bands;
+    float D = 0.027f;
+    float E = 0.5f-D;
+    float[] vertices = { -E,-E, +E,-E, +E,+E, -E,+E };
+    int extraI, extraV;
+
+    switch(size)
+      {
+      case 2 : bands = new float[] { 1.0f    ,-D,
+                                     1.0f-D/2,-D*0.55f,
+                                     1.0f-D  ,-D*0.25f,
+                                     1.0f-2*D, 0.0f,
+                                     0.50f, 0.040f,
+                                     0.0f, 0.048f };
+               extraI = 2;
+               extraV = 2;
+               break;
+      case 3 : bands = new float[] { 1.0f    ,-D,
+                                     1.0f-D*1.2f,-D*0.55f,
+                                     1.0f-2*D, 0.0f,
+                                     0.50f, 0.040f,
+                                     0.0f, 0.048f };
+               extraI = 2;
+               extraV = 2;
+               break;
+      case 4 : bands = new float[] { 1.0f    ,-D,
+                                     1.0f-D*1.2f,-D*0.55f,
+                                     1.0f-2*D, 0.0f,
+                                     0.50f, 0.040f,
+                                     0.0f, 0.048f };
+               extraI = 1;
+               extraV = 2;
+               break;
+      default: bands = new float[] { 1.0f    ,-D,
+                                     1.0f-2*D, 0.0f,
+                                     0.50f, 0.025f,
+                                     0.0f, 0.030f };
+               extraI = 1;
+               extraV = 1;
+               break;
+      }
+
+    return createCubitMesh(index,vertices,bands,extraI,extraV);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int index, float[] vertices, float[] bands, int extraI, int extraV)
+    {
+    if( mMeshes==null )
+      {
+      mMeshes = new MeshBase[ObjectList.CUBE.getNumVariants()];
+      }
+
+    if( mMeshes[index]==null )
+      {
+      final int MESHES=6;
+      int association = 1;
+      MeshBase[] meshes = new MeshPolygon[MESHES];
+      meshes[0] = new MeshPolygon(vertices,bands,extraI,extraV);
+      meshes[0].setEffectAssociation(0,association,0);
+
+      for(int i=1; i<MESHES; i++)
+        {
+        association <<=1;
+        meshes[i] = meshes[0].copy(true);
+        meshes[i].setEffectAssociation(0,association,0);
+        }
+
+      mMeshes[index] = new MeshJoined(meshes);
+
+      Static3D axisY   = new Static3D(0,1,0);
+      Static3D axisX   = new Static3D(1,0,0);
+      Static3D center  = new Static3D(0,0,0);
+      Static1D angle90 = new Static1D(90);
+      Static1D angle180= new Static1D(180);
+      Static1D angle270= new Static1D(270);
+
+      float d1 = 1.0f;
+      float d2 =-0.05f;
+      float d3 = 0.12f;
+
+      Static3D dCen0 = new Static3D( d1*(+0.5f), d1*(+0.5f), d1*(+0.5f) );
+      Static3D dCen1 = new Static3D( d1*(+0.5f), d1*(+0.5f), d1*(-0.5f) );
+      Static3D dCen2 = new Static3D( d1*(+0.5f), d1*(-0.5f), d1*(+0.5f) );
+      Static3D dCen3 = new Static3D( d1*(+0.5f), d1*(-0.5f), d1*(-0.5f) );
+      Static3D dCen4 = new Static3D( d1*(-0.5f), d1*(+0.5f), d1*(+0.5f) );
+      Static3D dCen5 = new Static3D( d1*(-0.5f), d1*(+0.5f), d1*(-0.5f) );
+      Static3D dCen6 = new Static3D( d1*(-0.5f), d1*(-0.5f), d1*(+0.5f) );
+      Static3D dCen7 = new Static3D( d1*(-0.5f), d1*(-0.5f), d1*(-0.5f) );
+
+      Static3D dVec0 = new Static3D( d2*(+0.5f), d2*(+0.5f), d2*(+0.5f) );
+      Static3D dVec1 = new Static3D( d2*(+0.5f), d2*(+0.5f), d2*(-0.5f) );
+      Static3D dVec2 = new Static3D( d2*(+0.5f), d2*(-0.5f), d2*(+0.5f) );
+      Static3D dVec3 = new Static3D( d2*(+0.5f), d2*(-0.5f), d2*(-0.5f) );
+      Static3D dVec4 = new Static3D( d2*(-0.5f), d2*(+0.5f), d2*(+0.5f) );
+      Static3D dVec5 = new Static3D( d2*(-0.5f), d2*(+0.5f), d2*(-0.5f) );
+      Static3D dVec6 = new Static3D( d2*(-0.5f), d2*(-0.5f), d2*(+0.5f) );
+      Static3D dVec7 = new Static3D( d2*(-0.5f), d2*(-0.5f), d2*(-0.5f) );
+
+      Static4D dReg  = new Static4D(0,0,0,d3);
+      Static1D dRad  = new Static1D(1);
+
+      VertexEffectMove   effect0 = new VertexEffectMove(new Static3D(0,0,+0.5f));
+      effect0.setMeshAssociation(63,-1);  // all 6 sides
+      VertexEffectRotate effect1 = new VertexEffectRotate( angle180, axisX, center );
+      effect1.setMeshAssociation(32,-1);  // back
+      VertexEffectRotate effect2 = new VertexEffectRotate( angle90 , axisX, center );
+      effect2.setMeshAssociation( 8,-1);  // bottom
+      VertexEffectRotate effect3 = new VertexEffectRotate( angle270, axisX, center );
+      effect3.setMeshAssociation( 4,-1);  // top
+      VertexEffectRotate effect4 = new VertexEffectRotate( angle270, axisY, center );
+      effect4.setMeshAssociation( 2,-1);  // left
+      VertexEffectRotate effect5 = new VertexEffectRotate( angle90 , axisY, center );
+      effect5.setMeshAssociation( 1,-1);  // right
+
+      VertexEffectDeform effect6 = new VertexEffectDeform(dVec0, dRad, dCen0, dReg);
+      VertexEffectDeform effect7 = new VertexEffectDeform(dVec1, dRad, dCen1, dReg);
+      VertexEffectDeform effect8 = new VertexEffectDeform(dVec2, dRad, dCen2, dReg);
+      VertexEffectDeform effect9 = new VertexEffectDeform(dVec3, dRad, dCen3, dReg);
+      VertexEffectDeform effect10= new VertexEffectDeform(dVec4, dRad, dCen4, dReg);
+      VertexEffectDeform effect11= new VertexEffectDeform(dVec5, dRad, dCen5, dReg);
+      VertexEffectDeform effect12= new VertexEffectDeform(dVec6, dRad, dCen6, dReg);
+      VertexEffectDeform effect13= new VertexEffectDeform(dVec7, dRad, dCen7, dReg);
+
+      mMeshes[index].apply(effect0);
+      mMeshes[index].apply(effect1);
+      mMeshes[index].apply(effect2);
+      mMeshes[index].apply(effect3);
+      mMeshes[index].apply(effect4);
+      mMeshes[index].apply(effect5);
+      mMeshes[index].apply(effect6);
+      mMeshes[index].apply(effect7);
+      mMeshes[index].apply(effect8);
+      mMeshes[index].apply(effect9);
+      mMeshes[index].apply(effect10);
+      mMeshes[index].apply(effect11);
+      mMeshes[index].apply(effect12);
+      mMeshes[index].apply(effect13);
+
+      mMeshes[index].mergeEffComponents();
+      }
+
+    return mMeshes[index].copy(true);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return getSize();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    int size = getSize();
+    float[] chances = new float[size];
+
+    for(int i=0; i<size; i++)
+      {
+      chances[i] = (i+1.0f) / size;
+      }
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 4;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return (int)(getSize()*offset);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-1);
+      return (newVector>=oldRotAxis ? newVector+1 : newVector);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    float rowFloat = rnd.nextFloat();
+
+    for(int row=0; row<mRowChances.length; row++)
+      {
+      if( rowFloat<=mRowChances[row] ) return row;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isSolved()
+    {
+    int index = CUBITS[0].mQuatIndex;
+
+    for(int i=1; i<NUM_CUBITS; i++)
+      {
+      if( !thereIsNoVisibleDifference(CUBITS[i], index) ) return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return if the Cubit, when rotated with its own mQuatScramble, would have looked any different
+// then if it were rotated by quaternion 'quat'.
+// No it is not so simple as the quats need to be the same - imagine a 4x4x4 cube where the two
+// middle squares get interchanged. No visible difference!
+//
+// So: this is true iff the cubit
+// a) is a corner or edge and the quaternions are the same
+// b) is inside one of the faces and after rotations by both quats it ends up on the same face.
+
+  private boolean thereIsNoVisibleDifference(Cubit cubit, int quatIndex)
+    {
+    if ( cubit.mQuatIndex == quatIndex ) return true;
+
+    int belongsToHowManyFaces = 0;
+    int size = getSize()-1;
+    float row;
+    final float MAX_ERROR = 0.01f;
+
+    for(int i=0; i<NUM_AXIS; i++)
+      {
+      row = cubit.mRotationRow[i];
+      if( (row     <MAX_ERROR && row     >-MAX_ERROR) ||
+          (row-size<MAX_ERROR && row-size>-MAX_ERROR)  ) belongsToHowManyFaces++;
+      }
+
+    switch(belongsToHowManyFaces)
+      {
+      case 0 : return true ;  // 'inside' cubit that does not lie on any face
+      case 1 :                // cubit that lies inside one of the faces
+               Static3D orig = cubit.getOrigPosition();
+               Static4D quat1 = QUATS[quatIndex];
+               Static4D quat2 = QUATS[cubit.mQuatIndex];
+
+               Static4D cubitCenter = new Static4D( orig.get0(), orig.get1(), orig.get2(), 0);
+               Static4D rotated1 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat1 );
+               Static4D rotated2 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat2 );
+
+               float row1, row2, row3, row4;
+               float ax,ay,az;
+               Static3D axis;
+               float x1 = rotated1.get0();
+               float y1 = rotated1.get1();
+               float z1 = rotated1.get2();
+               float x2 = rotated2.get0();
+               float y2 = rotated2.get1();
+               float z2 = rotated2.get2();
+
+               for(int i=0; i<NUM_AXIS; i++)
+                 {
+                 axis = ROTATION_AXIS[i];
+                 ax = axis.get0();
+                 ay = axis.get1();
+                 az = axis.get2();
+
+                 row1 = ((x1*ax + y1*ay + z1*az) - mStart) / mStep;
+                 row2 = ((x2*ax + y2*ay + z2*az) - mStart) / mStep;
+                 row3 = row1 - size;
+                 row4 = row2 - size;
+
+                 if( (row1<MAX_ERROR && row1>-MAX_ERROR && row2<MAX_ERROR && row2>-MAX_ERROR) ||
+                     (row3<MAX_ERROR && row3>-MAX_ERROR && row4<MAX_ERROR && row4>-MAX_ERROR)  )
+                   {
+                   return true;
+                   }
+                 }
+               return false;
+
+      default: return false;  // edge or corner
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// order: Up --> Right --> Front --> Down --> Left --> Back
+// (because the first implemented Solver - the two-phase Cube3 one - expects such order)
+//
+// Solved 3x3x3 Cube maps to "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"
+//
+// s : size of the cube; let index = a*s + b    (i.e. a,b = row,column)
+//
+// Up    :   index --> b<s-1 ? (s-1)*(s+4b)+a : 6*s*s -13*s +8 +a
+// Right :   index --> 6*s*s - 12*s + 7 - index
+// Front :   index --> if b==0  : s*s - 1 - index
+//                     if b==s-1: 6*s*s -11*s +6 - index
+//                     else
+//                         a==0: s*s + s-1 + 4*(b-1)*(s-1) + 2*(s-2) + s
+//                         else: s*s + s-1 + 4*(b-1)*(s-1) + 2*(s-1-a)
+// Down  :   index --> b==0 ? (s-1-a) : s*s + s-1 + 4*(b-1)*(s-1) - a
+// Left  :   index --> (s-1-a)*s + b
+// Back  :   index --> if b==s-1: s*(s-1-a)
+//                     if b==0  : 5*s*s -12*s + 8 + (s-1-a)*s
+//                     else
+//                        if a==s-1: s*s + 4*(s-2-b)*(s-1)
+//                        else     : s*s + 4*(s-2-b)*(s-1) + s + (s-2-a)*2
+
+  public String retObjectString()
+    {
+    StringBuilder objectString = new StringBuilder();
+    int size = getSize();
+    int len = size*size;
+    int cubitIndex=-1, row=-1, col=-1;
+    int color=-1, face=-1;
+
+    final int RIGHT= 0;
+    final int LEFT = 1;
+    final int UP   = 2;
+    final int DOWN = 3;
+    final int FRONT= 4;
+    final int BACK = 5;
+
+    // 'I' - interior, theoretically can happen
+    final char[] FACE_NAMES = { 'R', 'L', 'U', 'D', 'F', 'B', 'I'};
+
+    face = UP;
+
+    for(int i=0; i<len; i++)
+      {
+      row = i/size;
+      col = i%size;
+
+      cubitIndex = col<size-1 ? (size-1)*(size+4*col) + row : 6*size*size - 13*size + 8 + row;
+      color = getCubitFaceColorIndex(cubitIndex,face);
+      objectString.append(FACE_NAMES[color]);
+      }
+
+    face = RIGHT;
+
+    for(int i=0; i<len; i++)
+      {
+      cubitIndex = 6*size*size - 12*size +7 - i;
+      color = getCubitFaceColorIndex(cubitIndex,face);
+      objectString.append(FACE_NAMES[color]);
+      }
+
+     face = FRONT;
+
+    for(int i=0; i<len; i++)
+      {
+      row = i/size;
+      col = i%size;
+
+      if( col==size-1 ) cubitIndex = 6*size*size - 11*size + 6 -i;
+      else if( col==0 ) cubitIndex = size*size - 1 - i;
+      else
+        {
+        if( row==0 ) cubitIndex = size*size + size-1 + 4*(col-1)*(size-1) + 2*(size-2) + size;
+        else         cubitIndex = size*size + size-1 + 4*(col-1)*(size-1) + 2*(size-1-row);
+        }
+
+      color = getCubitFaceColorIndex(cubitIndex,face);
+      objectString.append(FACE_NAMES[color]);
+      }
+
+    face = DOWN;
+
+    for(int i=0; i<len; i++)
+      {
+      row = i/size;
+      col = i%size;
+
+      cubitIndex = col==0 ? size-1-row : size*size + size-1 + 4*(col-1)*(size-1) - row;
+      color = getCubitFaceColorIndex(cubitIndex,face);
+      objectString.append(FACE_NAMES[color]);
+      }
+
+    face = LEFT;
+
+    for(int i=0; i<len; i++)
+      {
+      row = i/size;
+      col = i%size;
+
+      cubitIndex = (size-1-row)*size + col;
+      color = getCubitFaceColorIndex(cubitIndex,face);
+      objectString.append(FACE_NAMES[color]);
+      }
+
+    face = BACK;
+
+    for(int i=0; i<len; i++)
+      {
+      row = i/size;
+      col = i%size;
+
+      if( col==size-1 ) cubitIndex = size*(size-1-row);
+      else if( col==0 ) cubitIndex = 5*size*size - 12*size + 8 + (size-1-row)*size;
+      else
+        {
+        if( row==size-1 ) cubitIndex = size*size + 4*(size-2-col)*(size-1);
+        else              cubitIndex = size*size + 4*(size-2-col)*(size-1) + size + 2*(size-2-row);
+        }
+
+      color = getCubitFaceColorIndex(cubitIndex,face);
+      objectString.append(FACE_NAMES[color]);
+      }
+
+    return objectString.toString();
+    }
+}
diff --git a/src/main/java/org/distorted/objects/TwistyDiamond.java b/src/main/java/org/distorted/objects/TwistyDiamond.java
new file mode 100644
index 00000000..597df701
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyDiamond.java
@@ -0,0 +1,404 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyDiamond extends TwistyObject
+{
+  private static final float SQ2 = (float)Math.sqrt(2);
+  private static final float SQ3 = (float)Math.sqrt(3);
+  private static final float SQ6 = (float)Math.sqrt(6);
+
+  private static final int FACES_PER_CUBIT =8;
+
+  // the four rotation axis of a Diamond. Must be normalized.
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(+SQ6/3,+SQ3/3,     0),
+           new Static3D(-SQ6/3,+SQ3/3,     0),
+           new Static3D(     0,+SQ3/3,+SQ6/3),
+           new Static3D(     0,+SQ3/3,-SQ6/3)
+         };
+
+  // the eight axis that determine the faces
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(+SQ6/3,+SQ3/3,     0), new Static3D(-SQ6/3,-SQ3/3,     0),
+           new Static3D(-SQ6/3,+SQ3/3,     0), new Static3D(+SQ6/3,-SQ3/3,     0),
+           new Static3D(     0,+SQ3/3,+SQ6/3), new Static3D(     0,-SQ3/3,-SQ6/3),
+           new Static3D(     0,+SQ3/3,-SQ6/3), new Static3D(     0,-SQ3/3,+SQ6/3)
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_YELLOW, COLOR_WHITE,
+           COLOR_BLUE  , COLOR_GREEN,
+           COLOR_RED   , COLOR_BROWN,
+           COLOR_PINK  , COLOR_VIOLET
+         };
+
+  // All legal rotation quats of a Diamond
+  private static final Static4D[] QUATS = new Static4D[]
+         {
+           new Static4D(  0.0f,  0.0f,   0.0f,  1.0f ),
+           new Static4D(  0.0f,  1.0f,   0.0f,  0.0f ),
+           new Static4D(+SQ2/2,  0.5f,   0.0f,  0.5f ),
+           new Static4D(-SQ2/2,  0.5f,   0.0f,  0.5f ),
+           new Static4D(  0.0f,  0.5f, +SQ2/2,  0.5f ),
+           new Static4D(  0.0f,  0.5f, -SQ2/2,  0.5f ),
+           new Static4D(+SQ2/2,  0.5f,   0.0f, -0.5f ),
+           new Static4D(-SQ2/2,  0.5f,   0.0f, -0.5f ),
+           new Static4D(  0.0f,  0.5f, +SQ2/2, -0.5f ),
+           new Static4D(  0.0f,  0.5f, -SQ2/2, -0.5f ),
+           new Static4D(+SQ2/2,  0.0f, -SQ2/2,  0.0f ),
+           new Static4D(-SQ2/2,  0.0f, -SQ2/2,  0.0f )
+         };
+
+  private static final float DIST = 0.50f;
+
+  // centers of the 6 octahedrons + 8 tetrahedrons ( i.e. of the all 14 cubits)
+  private static final Static3D[] CENTERS = new Static3D[]
+         {
+           new Static3D( DIST,          0, DIST ),
+           new Static3D( DIST,          0,-DIST ),
+           new Static3D(-DIST,          0,-DIST ),
+           new Static3D(-DIST,          0, DIST ),
+           new Static3D(    0, DIST*SQ2  ,    0 ),
+           new Static3D(    0,-DIST*SQ2  ,    0 ),
+
+           new Static3D(    0, DIST*SQ2/2, DIST ),
+           new Static3D( DIST, DIST*SQ2/2,    0 ),
+           new Static3D(    0, DIST*SQ2/2,-DIST ),
+           new Static3D(-DIST, DIST*SQ2/2,    0 ),
+           new Static3D(    0,-DIST*SQ2/2, DIST ),
+           new Static3D( DIST,-DIST*SQ2/2,    0 ),
+           new Static3D(    0,-DIST*SQ2/2,-DIST ),
+           new Static3D(-DIST,-DIST*SQ2/2,    0 )
+         };
+
+  // Colors of the faces of cubits. Each cubit has 8 faces
+  private static final int[][] mFaceMap = new int[][]
+         {
+           { 6,1,8,8, 2,5,8,8 },
+           { 8,1,3,8, 8,5,7,8 },
+           { 8,8,3,4, 8,8,7,0 },
+           { 6,8,8,4, 2,8,8,0 },
+           { 6,1,3,4, 8,8,8,8 },
+           { 8,8,8,8, 2,5,7,0 },
+
+           { 6,8,8,8, 8,8,8,8 },
+           { 1,8,8,8, 8,8,8,8 },
+           { 3,8,8,8, 8,8,8,8 },
+           { 4,8,8,8, 8,8,8,8 },
+           { 2,8,8,8, 8,8,8,8 },
+           { 5,8,8,8, 8,8,8,8 },
+           { 7,8,8,8, 8,8,8,8 },
+           { 0,8,8,8, 8,8,8,8 }
+         };
+
+  private static MeshBase mOctaMesh, mTetraMesh;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyDiamond(int size, Static4D quat, DistortedTexture texture,
+                MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, 60, quat, texture, mesh, effects, moves, ObjectList.DIAM, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createOctaMesh()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createTetraMesh()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumStickerTypes()
+    {
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getBasicStep()
+    {
+    return SQ6/6;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return FACES_PER_CUBIT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static3D[] getCubitPositions(int size)
+    {
+    return CENTERS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Static4D getQuat(int cubit)
+    {
+    switch(cubit)
+      {
+      case  0:
+      case  1:
+      case  2:
+      case  3:
+      case  4:
+      case  5:
+      case  6: return QUATS[0];                          // unit quat
+      case  7: return new Static4D( SQ2/2,0,0,SQ2/2);    //  90 along Y
+      case  8: return QUATS[1];                          // 180 along Y
+      case  9: return new Static4D(-SQ2/2,0,0,SQ2/2);    //  90 along Y
+      case 10: return new Static4D(     0,0,1,    0);    // 180 along Z
+      case 11: return new Static4D(0, SQ2/2,SQ2/2,0);    //
+      case 12: return new Static4D(     1,0,0,    0);    // 180 along X
+      case 13: return new Static4D(0,-SQ2/2,SQ2/2,0);    //
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    MeshBase mesh;
+
+    if( cubit<6 )
+      {
+      if( mOctaMesh==null ) createOctaMesh();
+      mesh = mOctaMesh.copy(true);
+      }
+    else
+      {
+      if( mTetraMesh==null ) createTetraMesh();
+      mesh = mTetraMesh.copy(true);
+      }
+
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( getQuat(cubit), new Static3D(0,0,0) );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    return mFaceMap[cubit][cubitface];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    float STROKE = 0.044f*side;
+    float OFF = STROKE/2 -1;
+    float OFF2 = 0.5f*side + OFF;
+    float HEIGHT = side - OFF;
+    float RADIUS = side/12.0f;
+    float ARC1_H = 0.2f*side;
+    float ARC1_W = side*0.5f;
+    float ARC2_W = 0.153f*side;
+    float ARC2_H = 0.905f*side;
+    float ARC3_W = side-ARC2_W;
+
+    float M = SQ3/2;
+    float D = (M/2 - 0.51f)*side;
+
+    paint.setAntiAlias(true);
+    paint.setStrokeWidth(STROKE);
+    paint.setColor(FACE_COLORS[face]);
+    paint.setStyle(Paint.Style.FILL);
+
+    canvas.drawRect(left,top,left+side,top+side,paint);
+
+    paint.setColor(INTERIOR_COLOR);
+    paint.setStyle(Paint.Style.STROKE);
+
+    canvas.drawLine(           left, M*HEIGHT+D,  side       +left, M*HEIGHT+D, paint);
+    canvas.drawLine(      OFF +left, M*side  +D,       OFF2  +left,          D, paint);
+    canvas.drawLine((side-OFF)+left, M*side  +D, (side-OFF2) +left,          D, paint);
+
+    canvas.drawArc( ARC1_W-RADIUS+left, M*(ARC1_H-RADIUS)+D, ARC1_W+RADIUS+left, M*(ARC1_H+RADIUS)+D, 225, 90, false, paint);
+    canvas.drawArc( ARC2_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC2_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 105, 90, false, paint);
+    canvas.drawArc( ARC3_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC3_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 345, 90, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 2.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    float[] chances = new float[2];
+
+    chances[0] = 0.5f;
+    chances[1] = 1.0f;
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return offset<0.25f ? 0:1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-1);
+      return (newVector>=oldRotAxis ? newVector+1 : newVector);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    float rowFloat = rnd.nextFloat();
+
+    for(int row=0; row<mRowChances.length; row++)
+      {
+      if( rowFloat<=mRowChances[row] ) return row;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// The Diamond is solved if and only if:
+//
+// ??
+
+  public boolean isSolved()
+    {
+
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// only needed for solvers - there are no Diamond solvers ATM)
+
+  public String retObjectString()
+    {
+    return "";
+    }
+
+}
diff --git a/src/main/java/org/distorted/objects/TwistyDino.java b/src/main/java/org/distorted/objects/TwistyDino.java
new file mode 100644
index 00000000..18c25978
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyDino.java
@@ -0,0 +1,463 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.effect.VertexEffectMove;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.effect.VertexEffectScale;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.RubikSurfaceView;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class TwistyDino extends TwistyObject
+{
+  private static final float SQ2 = (float)Math.sqrt(2);
+  private static final float SQ3 = (float)Math.sqrt(3);
+
+  // the four rotation axis of a RubikDino. Must be normalized.
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(+SQ3/3,+SQ3/3,+SQ3/3),
+           new Static3D(+SQ3/3,+SQ3/3,-SQ3/3),
+           new Static3D(+SQ3/3,-SQ3/3,+SQ3/3),
+           new Static3D(+SQ3/3,-SQ3/3,-SQ3/3)
+         };
+
+  // the six axis that determine the faces
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(1,0,0), new Static3D(-1,0,0),
+           new Static3D(0,1,0), new Static3D(0,-1,0),
+           new Static3D(0,0,1), new Static3D(0,0,-1)
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_YELLOW, COLOR_WHITE,
+           COLOR_BLUE  , COLOR_GREEN,
+           COLOR_RED   , COLOR_BROWN
+         };
+
+  // All legal rotation quats of a RubikDino
+  static final Static4D[] QUATS = new Static4D[]
+         {
+           new Static4D(  0.0f,  0.0f,  0.0f,  1.0f ),
+           new Static4D(  0.5f,  0.5f,  0.5f, -0.5f ),
+           new Static4D(  0.0f,  0.0f,  1.0f,  0.0f ),
+           new Static4D(  0.5f, -0.5f, -0.5f, -0.5f ),
+           new Static4D(  0.5f,  0.5f,  0.5f,  0.5f ),
+           new Static4D(  0.5f,  0.5f, -0.5f, -0.5f ),
+           new Static4D(  0.5f, -0.5f,  0.5f, -0.5f ),
+           new Static4D(  0.5f, -0.5f, -0.5f,  0.5f ),
+           new Static4D(  0.0f,  1.0f,  0.0f,  0.0f ),
+           new Static4D(  0.5f, -0.5f,  0.5f,  0.5f ),
+           new Static4D(  1.0f,  0.0f,  0.0f,  0.0f ),
+           new Static4D(  0.5f,  0.5f, -0.5f,  0.5f )
+         };
+
+  // centers of the 12 edges. Must be in the same order like QUATs above.
+  private static final Static3D[] CENTERS = new Static3D[]
+         {
+           new Static3D( 0.0f, 1.5f, 1.5f ),
+           new Static3D( 1.5f, 0.0f, 1.5f ),
+           new Static3D( 0.0f,-1.5f, 1.5f ),
+           new Static3D(-1.5f, 0.0f, 1.5f ),
+           new Static3D( 1.5f, 1.5f, 0.0f ),
+           new Static3D( 1.5f,-1.5f, 0.0f ),
+           new Static3D(-1.5f,-1.5f, 0.0f ),
+           new Static3D(-1.5f, 1.5f, 0.0f ),
+           new Static3D( 0.0f, 1.5f,-1.5f ),
+           new Static3D( 1.5f, 0.0f,-1.5f ),
+           new Static3D( 0.0f,-1.5f,-1.5f ),
+           new Static3D(-1.5f, 0.0f,-1.5f )
+         };
+
+  private static MeshBase mMesh;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyDino(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+             DistortedEffects effects, int[][] moves, ObjectList obj, Resources res, int scrWidth)
+    {
+    super(size, 60, quat, texture, mesh, effects, moves, obj, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createBasicMesh()
+    {
+    final float ANGLE = (float)((180/Math.PI)*(Math.atan(SQ2)));
+
+    final int MESHES=4;
+
+    float D = 0.02f;
+    float E = 0.5f*SQ2;
+    float F = 0.5f;
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-2*D, D*0.25f,
+                       1.0f-4*D, D*0.35f,
+                       1.0f-8*D, D*0.6f,
+                       0.60f   , D*1.0f,
+                       0.30f   , D*1.375f,
+                       0.0f    , D*1.4f };
+
+    float[] vertices0 = { -F,F/3, 0,-2*F/3, +F,F/3 };
+
+    MeshBase[] meshes = new MeshPolygon[MESHES];
+    meshes[0] = new MeshPolygon(vertices0, bands0, 2, 5);
+    meshes[0].setEffectAssociation(0,1,0);
+    meshes[1] = meshes[0].copy(true);
+    meshes[1].setEffectAssociation(0,2,0);
+
+    float[] bands1 = { 1.0f    , 0,
+                       0.50f   , 0.10f,
+                       0.0f    , 0.20f };
+
+    float[] vertices1 = { -E/2,-E*(SQ3/6), E/2,-E*(SQ3/6), 0,E*(SQ3/3) };
+
+    meshes[2] = new MeshPolygon(vertices1, bands1, 1, 2);
+    meshes[2].setEffectAssociation(0,4,0);
+    meshes[3] = meshes[2].copy(true);
+    meshes[3].setEffectAssociation(0,8,0);
+
+    mMesh = new MeshJoined(meshes);
+
+    Static3D a0 = new Static3D(     0,-3*F,    0 );
+    Static3D a1 = new Static3D(     0,   0, -3*F );
+    Static3D a2 = new Static3D(  -3*F,   0,    0 );
+    Static3D a3 = new Static3D(  +3*F,   0,    0 );
+
+    Static3D v0 = new Static3D(     0,-3*F/2, 3*F/2 );
+    Static3D v1 = new Static3D(     0, 3*F/2,-3*F/2 );
+    Static3D v2 = new Static3D(  -3*F, 3*F/2, 3*F/2 );
+    Static3D v3 = new Static3D(  +3*F, 3*F/2, 3*F/2 );
+
+    float d1 = 1.0f;
+    float d2 =-0.10f;
+    float d3 =-0.10f;
+    float d4 = 0.40f;
+
+    Static3D dCen0 = new Static3D( d1*a0.get0(), d1*a0.get1(), d1*a0.get2() );
+    Static3D dCen1 = new Static3D( d1*a1.get0(), d1*a1.get1(), d1*a1.get2() );
+    Static3D dCen2 = new Static3D( d1*a2.get0(), d1*a2.get1(), d1*a2.get2() );
+    Static3D dCen3 = new Static3D( d1*a3.get0(), d1*a3.get1(), d1*a3.get2() );
+
+    Static3D dVec0 = new Static3D( d3*v0.get0(), d3*v0.get1(), d3*v0.get2() );
+    Static3D dVec1 = new Static3D( d3*v1.get0(), d3*v1.get1(), d3*v1.get2() );
+    Static3D dVec2 = new Static3D( d2*v2.get0(), d2*v2.get1(), d2*v2.get2() );
+    Static3D dVec3 = new Static3D( d2*v3.get0(), d2*v3.get1(), d2*v3.get2() );
+
+    Static4D dReg  = new Static4D(0,0,0,d4);
+    Static1D dRad  = new Static1D(1);
+
+    Static1D angle1 = new Static1D(+ANGLE);
+    Static1D angle2 = new Static1D(-ANGLE);
+
+    Static3D axisX  = new Static3D(1,0,0);
+    Static3D axisY  = new Static3D(0,1,0);
+    Static3D axisZ  = new Static3D(0,-1,1);
+
+    Static3D center0= new Static3D(0,0,0);
+    Static3D center1= new Static3D(0,-3*F,0);
+
+    VertexEffectScale   effect0 = new VertexEffectScale ( new Static3D(3,3,3) );
+    VertexEffectMove    effect1 = new VertexEffectMove  ( new Static3D(0,-F,0) );
+    VertexEffectRotate  effect2 = new VertexEffectRotate( new Static1D(90), axisX, center0 );
+    VertexEffectScale   effect3 = new VertexEffectScale ( new Static3D(1,-1,1) );
+    VertexEffectMove    effect4 = new VertexEffectMove  ( new Static3D(3*E/2,E*(SQ3/2)-3*F,0) );
+    VertexEffectRotate  effect5 = new VertexEffectRotate( new Static1D(+90), axisY, center1 );
+    VertexEffectScale   effect6 = new VertexEffectScale ( new Static3D(-1,1,1) );
+    VertexEffectRotate  effect7 = new VertexEffectRotate( new Static1D( 45), axisX, center1 );
+    VertexEffectRotate  effect8 = new VertexEffectRotate( angle1           , axisZ, center1 );
+    VertexEffectRotate  effect9 = new VertexEffectRotate( angle2           , axisZ, center1 );
+
+    VertexEffectDeform  effect10= new VertexEffectDeform(dVec0, dRad, dCen0, dReg);
+    VertexEffectDeform  effect11= new VertexEffectDeform(dVec1, dRad, dCen1, dReg);
+    VertexEffectDeform  effect12= new VertexEffectDeform(dVec2, dRad, dCen2, dReg);
+    VertexEffectDeform  effect13= new VertexEffectDeform(dVec3, dRad, dCen3, dReg);
+
+    effect0.setMeshAssociation(15,-1);  // apply to meshes 0,1,2,3
+    effect1.setMeshAssociation( 3,-1);  // apply to meshes 0,1
+    effect2.setMeshAssociation( 2,-1);  // apply to mesh 1
+    effect3.setMeshAssociation( 2,-1);  // apply to mesh 0
+    effect4.setMeshAssociation(12,-1);  // apply to meshes 2,3
+    effect5.setMeshAssociation(12,-1);  // apply to meshes 2,3
+    effect6.setMeshAssociation( 8,-1);  // apply to mesh 3
+    effect7.setMeshAssociation(12,-1);  // apply to meshes 2,3
+    effect8.setMeshAssociation( 4,-1);  // apply to mesh 2
+    effect9.setMeshAssociation( 8,-1);  // apply to mesh 3
+    effect10.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
+    effect11.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
+    effect12.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
+    effect13.setMeshAssociation(15,-1); // apply to meshes 0,1,2,3
+
+    mMesh.apply(effect0);
+    mMesh.apply(effect1);
+    mMesh.apply(effect2);
+    mMesh.apply(effect3);
+    mMesh.apply(effect4);
+    mMesh.apply(effect5);
+    mMesh.apply(effect6);
+    mMesh.apply(effect7);
+    mMesh.apply(effect8);
+    mMesh.apply(effect9);
+    mMesh.apply(effect10);
+    mMesh.apply(effect11);
+    mMesh.apply(effect12);
+    mMesh.apply(effect13);
+
+    mMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int mulQuat(int q1, int q2)
+    {
+    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
+
+    float rX = result.get0();
+    float rY = result.get1();
+    float rZ = result.get2();
+    float rW = result.get3();
+
+    final float MAX_ERROR = 0.1f;
+    float dX,dY,dZ,dW;
+
+    for(int i=0; i<QUATS.length; i++)
+      {
+      dX = QUATS[i].get0() - rX;
+      dY = QUATS[i].get1() - rY;
+      dZ = QUATS[i].get2() - rZ;
+      dW = QUATS[i].get3() - rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+
+      dX = QUATS[i].get0() + rX;
+      dY = QUATS[i].get1() + rY;
+      dZ = QUATS[i].get2() + rZ;
+      dW = QUATS[i].get3() + rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.5f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getBasicStep()
+    {
+    return SQ3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumStickerTypes()
+    {
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return 4;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static3D[] getCubitPositions(int size)
+    {
+    return CENTERS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    if( mMesh==null ) createBasicMesh();
+
+    MeshBase mesh = mMesh.copy(true);
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( QUATS[cubit], new Static3D(0,0,0) );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    float STROKE = 0.04f*side;
+    float L= left;
+    float H= 0.333f*side;
+    float LEN = 0.5f*side;
+
+    paint.setAntiAlias(true);
+    paint.setStrokeWidth(STROKE);
+    paint.setColor(FACE_COLORS[face]);
+    paint.setStyle(Paint.Style.FILL);
+
+    canvas.drawRect(left,top,left+side,top+side,paint);
+
+    paint.setColor(INTERIOR_COLOR);
+    paint.setStyle(Paint.Style.STROKE);
+
+    canvas.drawLine( L      , H,  L+2*LEN, H    , paint);
+    canvas.drawLine( L      , H,  L+  LEN, H+LEN, paint);
+    canvas.drawLine( L+2*LEN, H,  L+  LEN, H+LEN, paint);
+
+    float S1 = 0.150f*side;
+    float S2 = 0.090f*side;
+    float X  = 0.7f*S2;
+    float Y  = 0.2f*S1;
+
+    float LA = left+0.500f*side;
+    float RA = left;
+    float TA = 0.333f*side;
+    float BA = 0.833f*side;
+
+    canvas.drawArc( RA+X        , TA     , RA+X+S2  , TA+S2, 135,135, false, paint);
+    canvas.drawArc( RA+side-S2-X, TA     , RA+side-X, TA+S2, 270,135, false, paint);
+    canvas.drawArc( LA-S1/2     , BA-S1-Y, LA+S1/2  , BA-Y ,  45, 90, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 2.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    float[] chances = new float[3];
+
+    chances[0] = 0.5f;
+    chances[1] = 0.5f;
+    chances[2] = 1.0f;
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return offset<0.5f ? 0:2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-1);
+      return (newVector>=oldRotAxis ? newVector+1 : newVector);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// only needed for solvers - there are no Dino solvers ATM)
+
+  public String retObjectString()
+    {
+    return "";
+    }
+
+}
diff --git a/src/main/java/org/distorted/objects/TwistyDino4.java b/src/main/java/org/distorted/objects/TwistyDino4.java
new file mode 100644
index 00000000..3761f379
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyDino4.java
@@ -0,0 +1,111 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static4D;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyDino4 extends TwistyDino
+{
+  private static final int[] mFaceMap = {4,4, 2,2, 2,2, 4,4,
+                                         0,0, 2,2, 1,1, 4,4,
+                                         0,0, 0,0, 1,1, 1,1 };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyDino4(int size, Static4D quat, DistortedTexture texture,
+              MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, quat, texture, mesh, effects, moves, ObjectList.DIN4, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    switch(cubitface)
+      {
+      case 0 : return mFaceMap[2*cubit];
+      case 1 : return mFaceMap[2*cubit+1];
+      default: return NUM_FACES;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    return (oldRotAxis==START_AXIS) ? ((newRotAxis==1 || newRotAxis==2) ? 0:2) : (oldRotAxis+newRotAxis==3 ? 2-oldRow : oldRow);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Dino4 is solved if and only if the four groups of three same-colored cubits are each 'together'
+// (actually we need to check only 3 first groups - if those are correct, the fourth one also needs
+// to be correct).
+//
+// White group : (X,Y,Z) = 10,11,6
+// Red group   : (X,Y,Z) = 0,3,7
+// Blue group  : (X,Y,Z) = 2,1,5
+// Yellow group: (X,Y,Z) = 8,9,4
+//
+// A group of 3 cubits is 'together' if and only if they are all rotated with one quat - but we cannot
+// forget that the whole Dino can be mirrored! (so then qY = qX*Q2 and qZ = qX*Q8 )
+//
+// X cubits: 0, 2, 8, 10
+// Y cubits: 1, 3, 9, 11
+// Z cubits: 4, 5, 6, 7
+
+  public boolean isSolved()
+    {
+    int redX = CUBITS[0].mQuatIndex;
+    int bluX = CUBITS[2].mQuatIndex;
+    int yelX = CUBITS[8].mQuatIndex;
+
+    if (CUBITS[3].mQuatIndex == redX && CUBITS[7].mQuatIndex == redX &&
+        CUBITS[1].mQuatIndex == bluX && CUBITS[5].mQuatIndex == bluX &&
+        CUBITS[9].mQuatIndex == yelX && CUBITS[4].mQuatIndex == yelX  ) return true;
+
+    if (CUBITS[3].mQuatIndex != mulQuat(redX,2)) return false;
+    if (CUBITS[7].mQuatIndex != mulQuat(redX,8)) return false;
+    if (CUBITS[1].mQuatIndex != mulQuat(bluX,2)) return false;
+    if (CUBITS[5].mQuatIndex != mulQuat(bluX,8)) return false;
+    if (CUBITS[9].mQuatIndex != mulQuat(yelX,2)) return false;
+    if (CUBITS[4].mQuatIndex != mulQuat(yelX,8)) return false;
+
+    return true;
+    }
+}
diff --git a/src/main/java/org/distorted/objects/TwistyDino6.java b/src/main/java/org/distorted/objects/TwistyDino6.java
new file mode 100644
index 00000000..288d3ef4
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyDino6.java
@@ -0,0 +1,108 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static4D;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyDino6 extends TwistyDino
+{
+  private static final int[] mFaceMap = {4,2, 0,4, 4,3, 1,4,
+                                         2,0, 3,0, 3,1, 2,1,
+                                         5,2, 0,5, 5,3, 1,5 };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyDino6(int size, Static4D quat, DistortedTexture texture,
+              MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, quat, texture, mesh, effects, moves, ObjectList.DINO, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    switch(cubitface)
+      {
+      case 0 : return mFaceMap[2*cubit];
+      case 1 : return mFaceMap[2*cubit+1];
+      default: return NUM_FACES;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    return (oldRotAxis==START_AXIS) ? (rnd.nextFloat()<=0.5f ? 0:2) : (oldRotAxis+newRotAxis==3 ? 2-oldRow : oldRow);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Dino6 is solved if and only if:
+//
+// All four 'X' cubits (i.e. those whose longest edge goes along the X axis) are rotated
+// by the same quaternion qX, similarly all four 'Y' cubits by the same qY and all four 'Z'
+// by the same qZ, and then either:
+//
+// a) qX = qY = qZ
+// b) qY = qX*Q2 and qZ = qX*Q8  (i.e. swap of WHITE and YELLOW faces)
+// c) qX = qY*Q2 and qZ = qY*Q10 (i.e. swap of BLUE and GREEN faces)
+// d) qX = qZ*Q8 and qY = qZ*Q10 (i.e. swap of RED and BROWN faces)
+//
+// BUT: cases b), c) and d) are really the same - it's all just a mirror image of the original.
+//
+// X cubits: 0, 2, 8, 10
+// Y cubits: 1, 3, 9, 11
+// Z cubits: 4, 5, 6, 7
+
+  public boolean isSolved()
+    {
+    int qX = CUBITS[0].mQuatIndex;
+    int qY = CUBITS[1].mQuatIndex;
+    int qZ = CUBITS[4].mQuatIndex;
+
+    if( CUBITS[2].mQuatIndex != qX || CUBITS[8].mQuatIndex != qX || CUBITS[10].mQuatIndex != qX ||
+        CUBITS[3].mQuatIndex != qY || CUBITS[9].mQuatIndex != qY || CUBITS[11].mQuatIndex != qY ||
+        CUBITS[5].mQuatIndex != qZ || CUBITS[6].mQuatIndex != qZ || CUBITS[ 7].mQuatIndex != qZ  )
+      {
+      return false;
+      }
+
+    return ( qX==qY && qX==qZ ) || ( qY==mulQuat(qX,2) && qZ==mulQuat(qX,8) );
+    }
+}
diff --git a/src/main/java/org/distorted/objects/TwistyHelicopter.java b/src/main/java/org/distorted/objects/TwistyHelicopter.java
new file mode 100644
index 00000000..7be6cfaa
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyHelicopter.java
@@ -0,0 +1,795 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.effect.VertexEffectMove;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.effect.VertexEffectScale;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.mesh.MeshTriangle;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.RubikSurfaceView;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyHelicopter extends TwistyObject
+{
+  private static final float SQ2 = (float)Math.sqrt(2);
+  private static final float SQ3 = (float)Math.sqrt(3);
+
+  private static final int FACES_PER_CUBIT =6;
+
+  // the six rotation axis of a Helicopter. Must be normalized.
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(     0, +SQ2/2, -SQ2/2),
+           new Static3D(     0, -SQ2/2, -SQ2/2),
+           new Static3D(+SQ2/2,      0, -SQ2/2),
+           new Static3D(-SQ2/2,      0, -SQ2/2),
+           new Static3D(+SQ2/2, -SQ2/2,      0),
+           new Static3D(-SQ2/2, -SQ2/2,      0)
+         };
+
+  // the six axis that determine the faces
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(1,0,0), new Static3D(-1,0,0),
+           new Static3D(0,1,0), new Static3D(0,-1,0),
+           new Static3D(0,0,1), new Static3D(0,0,-1)
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_YELLOW, COLOR_WHITE,
+           COLOR_BLUE  , COLOR_GREEN,
+           COLOR_RED   , COLOR_BROWN
+         };
+
+  // All legal rotation quats of a HELICOPTER (same as the Cube!)
+  private static final Static4D[] QUATS = new Static4D[]
+         {
+           new Static4D( 0.00f,  0.00f,  0.00f,  1.00f ),
+           new Static4D( 1.00f,  0.00f,  0.00f,  0.00f ),
+           new Static4D( 0.00f,  1.00f,  0.00f,  0.00f ),
+           new Static4D( 0.00f,  0.00f,  1.00f,  0.00f ),
+
+           new Static4D( SQ2/2,  SQ2/2,  0.00f,  0.00f ),
+           new Static4D( SQ2/2, -SQ2/2,  0.00f,  0.00f ),
+           new Static4D( SQ2/2,  0.00f,  SQ2/2,  0.00f ),
+           new Static4D( SQ2/2,  0.00f, -SQ2/2,  0.00f ),
+           new Static4D( SQ2/2,  0.00f,  0.00f,  SQ2/2 ),
+           new Static4D( SQ2/2,  0.00f,  0.00f, -SQ2/2 ),
+           new Static4D( 0.00f,  SQ2/2,  SQ2/2,  0.00f ),
+           new Static4D( 0.00f,  SQ2/2, -SQ2/2,  0.00f ),
+           new Static4D( 0.00f,  SQ2/2,  0.00f,  SQ2/2 ),
+           new Static4D( 0.00f,  SQ2/2,  0.00f, -SQ2/2 ),
+           new Static4D( 0.00f,  0.00f,  SQ2/2,  SQ2/2 ),
+           new Static4D( 0.00f,  0.00f,  SQ2/2, -SQ2/2 ),
+
+           new Static4D( 0.50f,  0.50f,  0.50f,  0.50f ),
+           new Static4D( 0.50f,  0.50f,  0.50f, -0.50f ),
+           new Static4D( 0.50f,  0.50f, -0.50f,  0.50f ),
+           new Static4D( 0.50f,  0.50f, -0.50f, -0.50f ),
+           new Static4D( 0.50f, -0.50f,  0.50f,  0.50f ),
+           new Static4D( 0.50f, -0.50f,  0.50f, -0.50f ),
+           new Static4D( 0.50f, -0.50f, -0.50f,  0.50f ),
+           new Static4D( 0.50f, -0.50f, -0.50f, -0.50f )
+         };
+
+  private static final float DIST_CORNER = 0.50f;
+  private static final float DIST_CENTER = 0.50f;
+  private static final float XY_CENTER   = DIST_CORNER/3;
+
+  // centers of the 8 corners + 6*4 face triangles ( i.e. of the all 32 cubits)
+  private static final Static3D[] CENTERS = new Static3D[]
+         {
+           new Static3D(   DIST_CORNER,   DIST_CORNER,   DIST_CORNER ),
+           new Static3D(   DIST_CORNER,   DIST_CORNER,  -DIST_CORNER ),
+           new Static3D(   DIST_CORNER,  -DIST_CORNER,   DIST_CORNER ),
+           new Static3D(   DIST_CORNER,  -DIST_CORNER,  -DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,   DIST_CORNER,   DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,   DIST_CORNER,  -DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,  -DIST_CORNER,   DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,  -DIST_CORNER,  -DIST_CORNER ),
+
+           new Static3D(   DIST_CENTER,     XY_CENTER,     XY_CENTER ),
+           new Static3D(   DIST_CENTER,     XY_CENTER,    -XY_CENTER ),
+           new Static3D(   DIST_CENTER,    -XY_CENTER,     XY_CENTER ),
+           new Static3D(   DIST_CENTER,    -XY_CENTER,    -XY_CENTER ),
+
+           new Static3D(  -DIST_CENTER,     XY_CENTER,     XY_CENTER ),
+           new Static3D(  -DIST_CENTER,     XY_CENTER,    -XY_CENTER ),
+           new Static3D(  -DIST_CENTER,    -XY_CENTER,     XY_CENTER ),
+           new Static3D(  -DIST_CENTER,    -XY_CENTER,    -XY_CENTER ),
+
+           new Static3D(   XY_CENTER  ,   DIST_CENTER,     XY_CENTER ),
+           new Static3D(   XY_CENTER  ,   DIST_CENTER,    -XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,   DIST_CENTER,     XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,   DIST_CENTER,    -XY_CENTER ),
+
+           new Static3D(   XY_CENTER  ,  -DIST_CENTER,     XY_CENTER ),
+           new Static3D(   XY_CENTER  ,  -DIST_CENTER,    -XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,  -DIST_CENTER,     XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,  -DIST_CENTER,    -XY_CENTER ),
+
+           new Static3D(   XY_CENTER  ,     XY_CENTER,   DIST_CENTER ),
+           new Static3D(   XY_CENTER  ,    -XY_CENTER,   DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,     XY_CENTER,   DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,    -XY_CENTER,   DIST_CENTER ),
+
+           new Static3D(   XY_CENTER  ,     XY_CENTER,  -DIST_CENTER ),
+           new Static3D(   XY_CENTER  ,    -XY_CENTER,  -DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,     XY_CENTER,  -DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,    -XY_CENTER,  -DIST_CENTER ),
+         };
+
+  // Colors of the faces of cubits. Each cubit has 6 faces
+  private static final int[][] mFaceMap = new int[][]
+         {
+           { 4,2,0, 6,6,6 },
+           { 0,2,5, 6,6,6 },
+           { 4,0,3, 6,6,6 },
+           { 5,3,0, 6,6,6 },
+           { 1,2,4, 6,6,6 },
+           { 5,2,1, 6,6,6 },
+           { 4,3,1, 6,6,6 },
+           { 1,3,5, 6,6,6 },
+
+           { 0 , 6,6,6,6,6 },
+           { 0 , 6,6,6,6,6 },
+           { 0 , 6,6,6,6,6 },
+           { 0 , 6,6,6,6,6 },
+
+           { 1 , 6,6,6,6,6 },
+           { 1 , 6,6,6,6,6 },
+           { 1 , 6,6,6,6,6 },
+           { 1 , 6,6,6,6,6 },
+
+           { 2 , 6,6,6,6,6 },
+           { 2 , 6,6,6,6,6 },
+           { 2 , 6,6,6,6,6 },
+           { 2 , 6,6,6,6,6 },
+
+           { 3 , 6,6,6,6,6 },
+           { 3 , 6,6,6,6,6 },
+           { 3 , 6,6,6,6,6 },
+           { 3 , 6,6,6,6,6 },
+
+           { 4 , 6,6,6,6,6 },
+           { 4 , 6,6,6,6,6 },
+           { 4 , 6,6,6,6,6 },
+           { 4 , 6,6,6,6,6 },
+
+           { 5 , 6,6,6,6,6 },
+           { 5 , 6,6,6,6,6 },
+           { 5 , 6,6,6,6,6 },
+           { 5 , 6,6,6,6,6 },
+         };
+
+  private static int[] QUAT_INDICES =
+      { 0,13,14,1,12,2,3,7,20,6,13,17,7,23,18,12,22,10,8,16,11,21,19,9,3,15,14,0,5,2,1,4 };
+
+  private static MeshBase mCornerMesh, mFaceMesh;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyHelicopter(int size, Static4D quat, DistortedTexture texture,
+                   MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, 60, quat, texture, mesh, effects, moves, ObjectList.HELI, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createCornerMesh()
+    {
+    float D = 0.02f;
+    float E = 0.5f;
+    float F = SQ2/4;
+
+    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-2*D, D*0.25f,
+                       1.0f-4*D, D*0.35f,
+                       1.0f-8*D, D*0.6f,
+                       0.60f   , D*1.0f,
+                       0.30f   , D*1.375f,
+                       0.0f    , D*1.4f };
+
+    MeshBase[] meshes = new MeshBase[6];
+
+    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
+    meshes[0].setEffectAssociation(0,1,0);
+    meshes[1] = meshes[0].copy(true);
+    meshes[1].setEffectAssociation(0,2,0);
+    meshes[2] = meshes[0].copy(true);
+    meshes[2].setEffectAssociation(0,4,0);
+
+    float[] vertices1 = { -F,-1.0f/12, +F,-1.0f/12, 0,1.0f/6 };
+    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
+
+    meshes[3] = new MeshPolygon(vertices1,bands1,1,5);
+    meshes[3].setEffectAssociation(0,8,0);
+    meshes[4] = meshes[3].copy(true);
+    meshes[4].setEffectAssociation(0,16,0);
+    meshes[5] = meshes[3].copy(true);
+    meshes[5].setEffectAssociation(0,32,0);
+
+    mCornerMesh = new MeshJoined(meshes);
+
+    Static3D axisX  = new Static3D(1,0,0);
+    Static3D axisY  = new Static3D(0,1,0);
+    Static3D axis0  = new Static3D(-SQ2/2,0,SQ2/2);
+    Static3D axis1  = new Static3D(+SQ3/3,+SQ3/3,+SQ3/3);
+    Static1D angle1 = new Static1D(+90);
+    Static1D angle2 = new Static1D(-90);
+    Static1D angle3 = new Static1D(-135);
+    Static1D angle4 = new Static1D(90);
+    Static1D angle5 = new Static1D(120);
+    Static1D angle6 = new Static1D(240);
+    Static3D center1= new Static3D(0,0,0);
+    Static3D center2= new Static3D(-0.25f,-0.25f,-0.25f);
+    Static3D move1  = new Static3D(-E/4,-E/4,0);
+    Static3D move2  = new Static3D(-0.25f,(-1.0f/6)-0.25f,-0.25f);
+
+    float d0 =-0.04f;
+    float d1 = 0.04f;
+    float r0 = 0.15f;
+    float r1 = 0.10f;
+
+    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
+    Static3D vec1   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(-SQ3/3));
+    Static3D vec2   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(-SQ3/3));
+    Static3D vec3   = new Static3D(d1*(-SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
+
+    Static1D radius = new Static1D(0.5f);
+
+    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
+    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
+    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
+    Static3D cent3  = new Static3D( 0.0f, 0.0f,-0.5f);
+
+    Static4D reg0   = new Static4D(0,0,0,r0);
+    Static4D reg1   = new Static4D(0,0,0,r1);
+
+    VertexEffectMove   effect0 = new VertexEffectMove(move1);
+    VertexEffectScale  effect1 = new VertexEffectScale(new Static3D(1,1,-1));
+    VertexEffectRotate effect2 = new VertexEffectRotate(angle1,axisX,center1);
+    VertexEffectRotate effect3 = new VertexEffectRotate(angle2,axisY,center1);
+    VertexEffectMove   effect4 = new VertexEffectMove(move2);
+    VertexEffectRotate effect5 = new VertexEffectRotate(angle1,axisX,center2);
+    VertexEffectRotate effect6 = new VertexEffectRotate(angle3,axisY,center2);
+    VertexEffectRotate effect7 = new VertexEffectRotate(angle4,axis0,center2);
+    VertexEffectRotate effect8 = new VertexEffectRotate(angle5,axis1,center2);
+    VertexEffectRotate effect9 = new VertexEffectRotate(angle6,axis1,center2);
+
+    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
+    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
+    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
+    VertexEffectDeform effect13= new VertexEffectDeform(vec3,radius,cent3,reg1);
+
+    effect0.setMeshAssociation( 7,-1);  // meshes 0,1,2
+    effect1.setMeshAssociation( 6,-1);  // meshes 1,2
+    effect2.setMeshAssociation( 2,-1);  // mesh 1
+    effect3.setMeshAssociation( 4,-1);  // mesh 2
+    effect4.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect5.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect6.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect7.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect8.setMeshAssociation(16,-1);  // mesh 4
+    effect9.setMeshAssociation(32,-1);  // mesh 5
+
+    effect10.setMeshAssociation(63,-1); // all meshes
+    effect11.setMeshAssociation(63,-1); // all meshes
+    effect12.setMeshAssociation(63,-1); // all meshes
+    effect13.setMeshAssociation(63,-1); // all meshes
+
+    mCornerMesh.apply(effect0);
+    mCornerMesh.apply(effect1);
+    mCornerMesh.apply(effect2);
+    mCornerMesh.apply(effect3);
+    mCornerMesh.apply(effect4);
+    mCornerMesh.apply(effect5);
+    mCornerMesh.apply(effect6);
+    mCornerMesh.apply(effect7);
+    mCornerMesh.apply(effect8);
+    mCornerMesh.apply(effect9);
+
+    mCornerMesh.apply(effect10);
+    mCornerMesh.apply(effect11);
+    mCornerMesh.apply(effect12);
+    mCornerMesh.apply(effect13);
+
+    mCornerMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createFaceMesh()
+    {
+    MeshBase[] meshes = new MeshBase[6];
+
+    float D = 0.02f;
+    float E = 0.5f;
+    float F = SQ2/4;
+    float G = 1.0f/12;
+
+    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-2*D, D*0.25f,
+                       1.0f-4*D, D*0.35f,
+                       1.0f-8*D, D*0.6f,
+                       0.60f   , D*1.0f,
+                       0.30f   , D*1.375f,
+                       0.0f    , D*1.4f };
+
+    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
+    meshes[0].setEffectAssociation(0,1,0);
+
+    float[] vertices1 = { -F,-G, +F,-G, 0,2*G};
+
+    float[] bands1 = { 1.0f   , 0.0f,
+                       0.5f   , 0.01f,
+                       0.0f   , 0.01f };
+
+    meshes[1] = new MeshPolygon(vertices1, bands1, 1, 3);
+    meshes[1].setEffectAssociation(0,2,0);
+
+    float[] vertices2 = { -E/2,-F/3, +E/2,-F/3, 0,2*F/3};
+
+    float[] bands2 = { 1.0f   , 0.0f,
+                       0.5f   , 0.01f,
+                       0.0f   , 0.01f };
+
+    meshes[2] = new MeshPolygon(vertices2, bands2, 1, 3);
+    meshes[2].setEffectAssociation(0,4,0);
+    meshes[3] = meshes[2].copy(true);
+    meshes[3].setEffectAssociation(0,8,0);
+    meshes[4] = new MeshTriangle(1);
+    meshes[4].setEffectAssociation(0,16,0);
+    meshes[5] = new MeshTriangle(1);
+    meshes[5].setEffectAssociation(0,32,0);
+
+    mFaceMesh = new MeshJoined(meshes);
+
+    Static3D move0  = new Static3D(-1.0f/8, -1.0f/8, 0);
+    Static3D move1  = new Static3D(-(SQ2/24)-1.0f/4, -(SQ2/24)-1.0f/4, 0);
+    Static3D move2  = new Static3D(-E/2, F/3, 0);
+    Static3D move3  = new Static3D(+E/2, F/3, 0);
+    Static3D move4  = new Static3D(+E/3,+E/3, 0);
+    Static1D angle1 = new Static1D(135);
+    Static1D angle2 = new Static1D(90);
+    Static1D angle3 = new Static1D(-90);
+    Static1D angle4 = new Static1D(-135);
+    Static3D axisX  = new Static3D(1,0,0);
+    Static3D axisY  = new Static3D(0,1,0);
+    Static3D axisZ  = new Static3D(0,0,1);
+    Static3D axis1  = new Static3D(1,-1,0);
+    Static3D center = new Static3D(0,0,0);
+    Static3D center1= new Static3D(-0.25f,-0.25f,0);
+
+    float d0 =-0.03f;
+    float d1 =-0.04f;
+    float r0 = 0.15f;
+    float r1 = 0.10f;
+
+    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
+    Static3D vec1   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(+SQ3/3));
+    Static3D vec2   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
+
+    Static1D radius = new Static1D(0.5f);
+
+    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
+    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
+    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
+
+    Static4D reg0   = new Static4D(0,0,0,r0);
+    Static4D reg1   = new Static4D(0,0,0,r1);
+
+    VertexEffectMove   effect0 = new VertexEffectMove(move0);
+    VertexEffectRotate effect1 = new VertexEffectRotate(angle1, axisZ, center);
+    VertexEffectMove   effect2 = new VertexEffectMove(move1);
+    VertexEffectRotate effect3 = new VertexEffectRotate(angle2, axis1, center1);
+    VertexEffectMove   effect4 = new VertexEffectMove(move2);
+    VertexEffectMove   effect5 = new VertexEffectMove(move3);
+    VertexEffectRotate effect6 = new VertexEffectRotate(angle3, axisZ, center);
+    VertexEffectRotate effect7 = new VertexEffectRotate(angle4, axisX, center);
+    VertexEffectRotate effect8 = new VertexEffectRotate(angle1, axisY, center);
+    VertexEffectScale  effect9 = new VertexEffectScale(0);
+    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
+    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
+    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
+    VertexEffectMove   effect13= new VertexEffectMove(move4);
+
+    effect0.setMeshAssociation( 1,-1);  // mesh 0
+    effect1.setMeshAssociation( 2,-1);  // mesh 1
+    effect2.setMeshAssociation( 2,-1);  // mesh 1
+    effect3.setMeshAssociation( 2,-1);  // mesh 1
+    effect4.setMeshAssociation( 4,-1);  // mesh 2
+    effect5.setMeshAssociation( 8,-1);  // mesh 3
+    effect6.setMeshAssociation( 8,-1);  // mesh 3
+    effect7.setMeshAssociation( 4,-1);  // mesh 2
+    effect8.setMeshAssociation( 8,-1);  // mesh 3
+    effect9.setMeshAssociation(48,-1);  // meshes 4,5
+    effect10.setMeshAssociation(15,-1); // meshes 0,1,2,3
+    effect11.setMeshAssociation(15,-1); // meshes 0,1,2,3
+    effect12.setMeshAssociation(15,-1); // meshes 0,1,2,3
+    effect13.setMeshAssociation(15,-1); // meshes 0,1,2,3
+
+    mFaceMesh.apply(effect0);
+    mFaceMesh.apply(effect1);
+    mFaceMesh.apply(effect2);
+    mFaceMesh.apply(effect3);
+    mFaceMesh.apply(effect4);
+    mFaceMesh.apply(effect5);
+    mFaceMesh.apply(effect6);
+    mFaceMesh.apply(effect7);
+    mFaceMesh.apply(effect8);
+    mFaceMesh.apply(effect9);
+    mFaceMesh.apply(effect10);
+    mFaceMesh.apply(effect11);
+    mFaceMesh.apply(effect12);
+    mFaceMesh.apply(effect13);
+
+    mFaceMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 1.5f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumStickerTypes()
+    {
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getBasicStep()
+    {
+    return SQ2/2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return FACES_PER_CUBIT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static3D[] getCubitPositions(int size)
+    {
+    return CENTERS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    MeshBase mesh;
+
+    if( cubit<8 )
+      {
+      if( mCornerMesh==null ) createCornerMesh();
+      mesh = mCornerMesh.copy(true);
+      }
+    else
+      {
+      if( mFaceMesh==null ) createFaceMesh();
+      mesh = mFaceMesh.copy(true);
+      }
+
+    int index = QUAT_INDICES[cubit];
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( QUATS[index], new Static3D(0,0,0) );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    return mFaceMap[cubit][cubitface];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    float STROKE = 0.035f*side;
+    float L= left+0.125f*side;
+    float H= 0.375f*side;
+    float LEN = 0.5f*side;
+
+    paint.setAntiAlias(true);
+    paint.setStrokeWidth(STROKE);
+    paint.setColor(FACE_COLORS[face]);
+    paint.setStyle(Paint.Style.FILL);
+
+    canvas.drawRect(left,top,left+side,top+side,paint);
+
+    paint.setColor(INTERIOR_COLOR);
+    paint.setStyle(Paint.Style.STROKE);
+
+    canvas.drawLine( L    , H,  L+LEN, H    , paint);
+    canvas.drawLine( L    , H,  L+LEN, H+LEN, paint);
+    canvas.drawLine( L+LEN, H,  L+LEN, H+LEN, paint);
+
+    float S1 = 0.125f*side;
+    float S2 = 0.070f*side;
+    float X  = 0.7f*S2;
+
+    float LA = left+0.625f*side;
+    float RA = left+0.125f*side;
+    float TA = 0.375f*side;
+    float BA = 0.875f*side;
+
+    canvas.drawArc( LA-S1, TA     , LA     , TA+S1, 270, 90, false, paint);
+    canvas.drawArc( RA+X , TA     , RA+X+S2, TA+S2, 135,135, false, paint);
+    canvas.drawArc( LA-S2, BA-X-S2, LA     , BA-X ,   0,135, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 2.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    float[] chances = new float[3];
+
+    chances[0] = 0.5f;
+    chances[1] = 0.5f;
+    chances[2] = 1.0f;
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return offset<0.166f ? 0:2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-2);
+
+      switch(oldRotAxis)
+        {
+        case  0:
+        case  1: return newVector+2;
+        case  2:
+        case  3: return (newVector==0 || newVector==1) ? newVector:newVector+2;
+        default: return newVector;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    float rowFloat = rnd.nextFloat();
+
+    for(int row=0; row<mRowChances.length; row++)
+      {
+      if( rowFloat<=mRowChances[row] ) return row;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// remember about the double cover or unit quaternions!
+
+  private int mulQuat(int q1, int q2)
+    {
+    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
+
+    float rX = result.get0();
+    float rY = result.get1();
+    float rZ = result.get2();
+    float rW = result.get3();
+
+    final float MAX_ERROR = 0.1f;
+    float dX,dY,dZ,dW;
+
+    for(int i=0; i<QUATS.length; i++)
+      {
+      dX = QUATS[i].get0() - rX;
+      dY = QUATS[i].get1() - rY;
+      dZ = QUATS[i].get2() - rZ;
+      dW = QUATS[i].get3() - rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+
+      dX = QUATS[i].get0() + rX;
+      dY = QUATS[i].get1() + rY;
+      dZ = QUATS[i].get2() + rZ;
+      dW = QUATS[i].get3() + rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// The Helicopter is solved if and only if:
+//
+// 1) all of its corner cubits are rotated with the same quat
+// 2) all its face cubits are rotated with the same quat like the corner ones,
+//    and optionally they also might be turned by a multiple of 90 degrees along
+//    a vector perpendicular to the face they lie on.
+//
+// i.e.
+// cubits  8, 9,10,11,12,13,14,15 - might be extra QUAT 1,8,9
+// cubits 16,17,18,19,20,21,22,23 - might be extra QUAT 2,12,13
+// cubits 24,25,26,27,28,29,30,31 - might be extra QUAT 3,14,15
+
+  public boolean isSolved()
+    {
+    int q = CUBITS[0].mQuatIndex;
+
+    if ( CUBITS[1].mQuatIndex == q &&
+         CUBITS[2].mQuatIndex == q &&
+         CUBITS[3].mQuatIndex == q &&
+         CUBITS[4].mQuatIndex == q &&
+         CUBITS[5].mQuatIndex == q &&
+         CUBITS[6].mQuatIndex == q &&
+         CUBITS[7].mQuatIndex == q  )
+      {
+      int q1 = mulQuat(q,1);
+      int q2 = mulQuat(q,8);
+      int q3 = mulQuat(q,9);
+
+      for(int index=8; index<16; index++)
+        {
+        int qIndex = CUBITS[index].mQuatIndex;
+        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
+        }
+
+      q1 = mulQuat(q, 2);
+      q2 = mulQuat(q,12);
+      q3 = mulQuat(q,13);
+
+      for(int index=16; index<24; index++)
+        {
+        int qIndex = CUBITS[index].mQuatIndex;
+        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
+        }
+
+      q1 = mulQuat(q, 3);
+      q2 = mulQuat(q,14);
+      q3 = mulQuat(q,15);
+
+      for(int index=24; index<32; index++)
+        {
+        int qIndex = CUBITS[index].mQuatIndex;
+        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
+        }
+
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// only needed for solvers - there are no Helicopter solvers ATM)
+
+  public String retObjectString()
+    {
+    return "";
+    }
+
+}
diff --git a/src/main/java/org/distorted/objects/TwistyObject.java b/src/main/java/org/distorted/objects/TwistyObject.java
new file mode 100644
index 00000000..b4eec3ff
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyObject.java
@@ -0,0 +1,718 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.MatrixEffectMove;
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.MatrixEffectScale;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedNode;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshFile;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic1D;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class TwistyObject extends DistortedNode
+  {
+  static final int COLOR_YELLOW = 0xffffff00;
+  static final int COLOR_WHITE  = 0xffffffff;
+  static final int COLOR_BLUE   = 0xff0000ff;
+  static final int COLOR_GREEN  = 0xff00ff00;
+  static final int COLOR_RED    = 0xffff0000;
+  static final int COLOR_BROWN  = 0xffb5651d;
+  static final int COLOR_PINK   = 0xffe134eb;
+  static final int COLOR_VIOLET = 0xffa534eb;
+
+  private static final float NODE_RATIO = 1.32f;
+  private static final float MAX_SIZE_CHANGE = 1.3f;
+  private static final float MIN_SIZE_CHANGE = 0.8f;
+
+  private static boolean mCreateFromDMesh = true;
+
+  private static final Static3D CENTER = new Static3D(0,0,0);
+  static final int INTERIOR_COLOR = 0xff000000;
+  private static final int POST_ROTATION_MILLISEC = 500;
+  private static final int TEXTURE_HEIGHT = 256;
+
+  final Static3D[] ROTATION_AXIS;
+  final Static4D[] QUATS;
+  final Cubit[] CUBITS;
+  final int NUM_FACES;
+  final int NUM_TEXTURES;
+  final int NUM_CUBIT_FACES;
+  final int NUM_AXIS;
+  final int NUM_CUBITS;
+  final float BASIC_STEP;
+
+  private static float mInitScreenRatio;
+  private static float mObjectScreenRatio = 1.0f;
+
+  private final int mNodeSize;
+  private int mRotRowBitmap;
+  private int mRotAxis;
+  private Static3D[] mOrigPos;
+  private Static3D mNodeScale;
+  private Static4D mQuat;
+  private int mSize;
+  private ObjectList mList;
+  private MeshBase mMesh;
+  private DistortedEffects mEffects;
+  private VertexEffectRotate mRotateEffect;
+  private Dynamic1D mRotationAngle;
+  private Static3D mRotationAxis;
+  private Static3D mObjectScale;
+
+  float mStart, mStep;
+  float[] mRowChances;
+
+  Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
+  DistortedTexture mTexture;
+
+  MatrixEffectScale mScaleEffect;
+  MatrixEffectQuaternion mQuatEffect;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyObject(int size, int fov, Static4D quat, DistortedTexture nodeTexture, MeshSquare nodeMesh,
+               DistortedEffects nodeEffects, int[][] moves, ObjectList list, Resources res, int screenWidth)
+    {
+    super(nodeTexture,nodeEffects,nodeMesh);
+
+    mNodeSize = screenWidth;
+
+    resizeFBO(mNodeSize, (int)(NODE_RATIO*mNodeSize));
+
+    mList = list;
+    mOrigPos = getCubitPositions(size);
+
+    QUATS = getQuats();
+    NUM_CUBITS  = mOrigPos.length;
+    ROTATION_AXIS = getRotationAxis();
+    NUM_AXIS = ROTATION_AXIS.length;
+    mInitScreenRatio = getScreenRatio();
+    NUM_FACES = getNumFaces();
+    NUM_CUBIT_FACES = getNumCubitFaces();
+    NUM_TEXTURES = getNumStickerTypes()*NUM_FACES;
+    BASIC_STEP = getBasicStep();
+
+    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
+    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
+
+    mSize = size;
+    computeStartAndStep(mOrigPos);
+    mNodeScale= new Static3D(1,NODE_RATIO,1);
+    mQuat = quat;
+
+    mRowChances = getRowChances();
+
+    mRotationAngle= new Dynamic1D();
+    mRotationAxis = new Static3D(1,0,0);
+    mRotateEffect = new VertexEffectRotate(mRotationAngle, mRotationAxis, CENTER);
+
+    mRotationAngleStatic = new Static1D(0);
+    mRotationAngleMiddle = new Static1D(0);
+    mRotationAngleFinal  = new Static1D(0);
+
+    float scale  = mObjectScreenRatio*mInitScreenRatio*mNodeSize/mSize;
+    mObjectScale = new Static3D(scale,scale,scale);
+    mScaleEffect = new MatrixEffectScale(mObjectScale);
+    mQuatEffect  = new MatrixEffectQuaternion(quat, CENTER);
+
+    MatrixEffectScale nodeScaleEffect = new MatrixEffectScale(mNodeScale);
+    nodeEffects.apply(nodeScaleEffect);
+
+    CUBITS = new Cubit[NUM_CUBITS];
+    createMeshAndCubits(list,res);
+
+    mTexture = new DistortedTexture();
+    mEffects = new DistortedEffects();
+
+    int num_quats = QUATS.length;
+    for(int q=0; q<num_quats; q++)
+      {
+      VertexEffectQuaternion vq = new VertexEffectQuaternion(QUATS[q],CENTER);
+      vq.setMeshAssociation(0,q);
+      mEffects.apply(vq);
+      }
+
+    mEffects.apply(mRotateEffect);
+    mEffects.apply(mQuatEffect);
+    mEffects.apply(mScaleEffect);
+
+    // Now postprocessed effects (the glow when you solve an object) require component centers. In
+    // order for the effect to be in front of the object, we need to set the center to be behind it.
+    getMesh().setComponentCenter(0,0,0,-0.1f);
+
+    attach( new DistortedNode(mTexture,mEffects,mMesh) );
+
+    setupPosition(moves);
+
+    setProjection(fov, 0.1f);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createMeshAndCubits(ObjectList list, Resources res)
+    {
+    if( mCreateFromDMesh )
+      {
+      int sizeIndex = ObjectList.getSizeIndex(list.ordinal(),mSize);
+      int resourceID= list.getResourceIDs()[sizeIndex];
+
+      InputStream is = res.openRawResource(resourceID);
+      DataInputStream dos = new DataInputStream(is);
+      mMesh = new MeshFile(dos);
+
+      try
+        {
+        is.close();
+        }
+      catch(IOException e)
+        {
+        android.util.Log.e("meshFile", "Error closing InputStream: "+e.toString());
+        }
+
+      for(int i=0; i<NUM_CUBITS; i++)
+        {
+        CUBITS[i] = new Cubit(this,mOrigPos[i]);
+        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(), 0);
+        }
+
+      if( shouldResetTextureMaps() ) resetAllTextureMaps();
+      }
+    else
+      {
+      MeshBase[] cubitMesh = new MeshBase[NUM_CUBITS];
+
+      for(int i=0; i<NUM_CUBITS; i++)
+        {
+        CUBITS[i] = new Cubit(this,mOrigPos[i]);
+        cubitMesh[i] = createCubitMesh(i);
+        cubitMesh[i].apply(new MatrixEffectMove(mOrigPos[i]),1,0);
+        cubitMesh[i].setEffectAssociation(0, CUBITS[i].computeAssociation(), 0);
+        }
+
+      mMesh = new MeshJoined(cubitMesh);
+      resetAllTextureMaps();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setObjectRatio(float sizeChange)
+    {
+    mObjectScreenRatio *= (1.0f+sizeChange)/2;
+
+    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
+    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
+
+    float scale = mObjectScreenRatio*mInitScreenRatio*mNodeSize/mSize;
+    mObjectScale.set(scale,scale,scale);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static float getObjectRatio()
+    {
+    return mObjectScreenRatio*mInitScreenRatio;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Cast centers of all Cubits on the first rotation Axis and compute the leftmost and rightmost
+// one. From there compute the 'start' (i.e. the leftmost) and 'step' (i.e. distance between two
+// consecutive).
+// it is assumed that other rotation axis have the same 'start' and 'step' - this is the case with
+// the Cube and the Pyraminx.
+// Start and Step are then needed to compute which rotation row (with respect to a given axis) a
+// given Cubit belongs to.
+
+  private void computeStartAndStep(Static3D[] pos)
+    {
+    float min = Float.MAX_VALUE;
+    float max = Float.MIN_VALUE;
+    float axisX = ROTATION_AXIS[0].get0();
+    float axisY = ROTATION_AXIS[0].get1();
+    float axisZ = ROTATION_AXIS[0].get2();
+    float tmp;
+
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      tmp = pos[i].get0()*axisX + pos[i].get1()*axisY + pos[i].get2()*axisZ;
+      if( tmp<min ) min=tmp;
+      if( tmp>max ) max=tmp;
+      }
+
+    mStart = min;
+    mStep  = (max-min+BASIC_STEP)/mSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean belongsToRotation( int cubit, int axis, int rowBitmap)
+    {
+    int cubitRow = (int)(CUBITS[cubit].mRotationRow[axis]+0.5f);
+    return ((1<<cubitRow)&rowBitmap)!=0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// note the minus in front of the sin() - we rotate counterclockwise
+// when looking towards the direction where the axis increases in values.
+
+  private Static4D makeQuaternion(int axisIndex, int angleInDegrees)
+    {
+    Static3D axis = ROTATION_AXIS[axisIndex];
+
+    while( angleInDegrees<0 ) angleInDegrees += 360;
+    angleInDegrees %= 360;
+    
+    float cosA = (float)Math.cos(Math.PI*angleInDegrees/360);
+    float sinA =-(float)Math.sqrt(1-cosA*cosA);
+
+    return new Static4D(axis.get0()*sinA, axis.get1()*sinA, axis.get2()*sinA, cosA);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private synchronized void setupPosition(int[][] moves)
+    {
+    if( moves!=null )
+      {
+      Static4D quat;
+      int index, axis, rowBitmap, angle;
+      int corr = (360/getBasicAngle());
+
+      for(int[] move: moves)
+        {
+        axis     = move[0];
+        rowBitmap= move[1];
+        angle    = move[2]*corr;
+        quat     = makeQuaternion(axis,angle);
+
+        for(int j=0; j<NUM_CUBITS; j++)
+          if( belongsToRotation(j,axis,rowBitmap) )
+            {
+            index = CUBITS[j].removeRotationNow(quat);
+            mMesh.setEffectAssociation(j, CUBITS[j].computeAssociation(),index);
+            }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// we cannot use belongsToRotation for deciding if to texture a face. Counterexample: the 'rotated'
+// tetrahedrons of Pyraminx nearby the edge: they belong to rotation but their face which is rotated
+// away from the face of the Pyraminx shouldn't be textured.
+
+  boolean isOnFace( int cubit, int axis, int row)
+    {
+    final float MAX_ERROR = 0.0001f;
+    float diff = CUBITS[cubit].mRotationRow[axis] - row;
+    return diff*diff < MAX_ERROR;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getCubitFaceColorIndex(int cubit, int face)
+    {
+    Static4D texMap = mMesh.getTextureMap(NUM_FACES*cubit + face);
+    return (int)(texMap.get0() / texMap.get2());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Clamp all rotated positions to one of those original ones to avoid accumulating errors.
+
+  void clampPos(Static3D pos)
+    {
+    float currError, minError = Float.MAX_VALUE;
+    int minErrorIndex= -1;
+    float x = pos.get0();
+    float y = pos.get1();
+    float z = pos.get2();
+    float xo,yo,zo;
+
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      xo = mOrigPos[i].get0();
+      yo = mOrigPos[i].get1();
+      zo = mOrigPos[i].get2();
+
+      currError = (xo-x)*(xo-x) + (yo-y)*(yo-y) + (zo-z)*(zo-z);
+
+      if( currError<minError )
+        {
+        minError = currError;
+        minErrorIndex = i;
+        }
+      }
+
+    pos.set( mOrigPos[minErrorIndex] );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// the getFaceColors + final black in a horizontal strip.
+
+  public void createTexture()
+    {
+    Bitmap bitmap;
+
+    Paint paint = new Paint();
+    bitmap = Bitmap.createBitmap( (NUM_TEXTURES+1)*TEXTURE_HEIGHT, TEXTURE_HEIGHT, Bitmap.Config.ARGB_8888);
+    Canvas canvas = new Canvas(bitmap);
+
+    paint.setAntiAlias(true);
+    paint.setTextAlign(Paint.Align.CENTER);
+    paint.setStyle(Paint.Style.FILL);
+
+    paint.setColor(INTERIOR_COLOR);
+    canvas.drawRect(0, 0, (NUM_TEXTURES+1)*TEXTURE_HEIGHT, TEXTURE_HEIGHT, paint);
+
+    for(int i=0; i<NUM_TEXTURES; i++)
+      {
+      createFaceTexture(canvas, paint, i, i*TEXTURE_HEIGHT, 0, TEXTURE_HEIGHT);
+      }
+
+    mTexture.setTexture(bitmap);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getSize()
+    {
+    return mSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void continueRotation(float angleInDegrees)
+    {
+    mRotationAngleStatic.set0(angleInDegrees);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static4D getRotationQuat()
+      {
+      return mQuat;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void recomputeScaleFactor(int scrWidth, int scrHeight)
+    {
+    mNodeScale.set(scrWidth,NODE_RATIO*scrWidth,scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+    for(int i=0; i<NUM_CUBITS; i++) CUBITS[i].savePreferences(editor);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void restorePreferences(SharedPreferences preferences)
+    {
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      int index = CUBITS[i].restorePreferences(preferences);
+      mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),index);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void releaseResources()
+    {
+    mTexture.markForDeletion();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void apply(Effect effect, int position)
+    {
+    mEffects.apply(effect, position);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void remove(long effectID)
+    {
+    mEffects.abortById(effectID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void solve()
+    {
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      CUBITS[i].solve();
+      mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(), 0);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void resetAllTextureMaps()
+    {
+    final float ratio = 1.0f/(NUM_TEXTURES+1);
+    int color;
+
+    for(int cubit=0; cubit<NUM_CUBITS; cubit++)
+      {
+      final Static4D[] maps = new Static4D[NUM_CUBIT_FACES];
+
+      for(int cubitface=0; cubitface<NUM_CUBIT_FACES; cubitface++)
+        {
+        color = getFaceColor(cubit,cubitface,mSize);
+        maps[cubitface] = new Static4D( color*ratio, 0.0f, ratio, 1.0f);
+        }
+
+      mMesh.setTextureMap(maps,NUM_CUBIT_FACES*cubit);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setTextureMap(int cubit, int face, int newColor)
+    {
+    final float ratio = 1.0f/(NUM_TEXTURES+1);
+    final Static4D[] maps = new Static4D[NUM_CUBIT_FACES];
+
+    maps[face] = new Static4D( newColor*ratio, 0.0f, ratio, 1.0f);
+    mMesh.setTextureMap(maps,NUM_CUBIT_FACES*cubit);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void beginNewRotation(int axis, int row )
+    {
+    if( axis<0 || axis>=ROTATION_AXIS.length )
+      {
+      android.util.Log.e("object", "invalid rotation axis: "+axis);
+      return;
+      }
+    if( row<0 || row>=mSize )
+      {
+      android.util.Log.e("object", "invalid rotation row: "+row);
+      return;
+      }
+
+    mRotAxis     = axis;
+    mRotRowBitmap= (1<<row);
+    mRotationAngleStatic.set0(0.0f);
+    mRotationAxis.set( ROTATION_AXIS[axis] );
+    mRotationAngle.add(mRotationAngleStatic);
+    mRotateEffect.setMeshAssociation( mRotRowBitmap<<(axis* ObjectList.MAX_OBJECT_SIZE) , -1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized long addNewRotation( int axis, int rowBitmap, int angle, long durationMillis, EffectListener listener )
+    {
+    mRotAxis     = axis;
+    mRotRowBitmap= rowBitmap;
+
+    mRotationAngleStatic.set0(0.0f);
+    mRotationAxis.set( ROTATION_AXIS[axis] );
+    mRotationAngle.setDuration(durationMillis);
+    mRotationAngle.resetToBeginning();
+    mRotationAngle.add(new Static1D(0));
+    mRotationAngle.add(new Static1D(angle));
+    mRotateEffect.setMeshAssociation( mRotRowBitmap<<(axis* ObjectList.MAX_OBJECT_SIZE) , -1);
+    mRotateEffect.notifyWhenFinished(listener);
+
+    return mRotateEffect.getID();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public long finishRotationNow(EffectListener listener, int nearestAngleInDegrees)
+    {
+    float angle = getAngle();
+    mRotationAngleStatic.set0(angle);
+    mRotationAngleFinal.set0(nearestAngleInDegrees);
+    mRotationAngleMiddle.set0( nearestAngleInDegrees + (nearestAngleInDegrees-angle)*0.2f );
+
+    mRotationAngle.setDuration(POST_ROTATION_MILLISEC);
+    mRotationAngle.resetToBeginning();
+    mRotationAngle.removeAll();
+    mRotationAngle.add(mRotationAngleStatic);
+    mRotationAngle.add(mRotationAngleMiddle);
+    mRotationAngle.add(mRotationAngleFinal);
+    mRotateEffect.notifyWhenFinished(listener);
+
+    return mRotateEffect.getID();
+    }
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float getAngle()
+    {
+    int pointNum = mRotationAngle.getNumPoints();
+
+    if( pointNum>=1 )
+      {
+      return mRotationAngle.getPoint(pointNum-1).get0();
+      }
+    else
+      {
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.log("points in RotationAngle: "+pointNum);
+      return 0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void removeRotationNow()
+    {
+    float angle = getAngle();
+    double nearestAngleInRadians = angle*Math.PI/180;
+    float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
+    float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
+    float axisX = ROTATION_AXIS[mRotAxis].get0();
+    float axisY = ROTATION_AXIS[mRotAxis].get1();
+    float axisZ = ROTATION_AXIS[mRotAxis].get2();
+    Static4D quat = new Static4D( axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
+
+    mRotationAngle.removeAll();
+    mRotationAngleStatic.set0(0);
+
+    for(int i=0; i<NUM_CUBITS; i++)
+      if( belongsToRotation(i,mRotAxis,mRotRowBitmap) )
+        {
+        int index = CUBITS[i].removeRotationNow(quat);
+        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),index);
+        }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void initializeObject(int[][] moves)
+    {
+    solve();
+    setupPosition(moves);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubit(float[] point3D)
+    {
+    float dist, minDist = Float.MAX_VALUE;
+    int currentBest=-1;
+    float multiplier = returnMultiplier();
+
+    point3D[0] *= multiplier;
+    point3D[1] *= multiplier;
+    point3D[2] *= multiplier;
+
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      dist = CUBITS[i].getDistSquared(point3D);
+      if( dist<minDist )
+        {
+        minDist = dist;
+        currentBest = i;
+        }
+      }
+
+    return currentBest;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeNearestAngle(float angle, float speed)
+    {
+    final int NEAREST = 360/getBasicAngle();
+
+    int tmp = (int)((angle+NEAREST/2)/NEAREST);
+    if( angle< -(NEAREST*0.5) ) tmp-=1;
+
+    if( tmp!=0 ) return NEAREST*tmp;
+
+    return speed> 1.2f ? NEAREST*(angle>0 ? 1:-1) : 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNodeSize()
+    {
+    return mNodeSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectList getObjectList()
+    {
+    return mList;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract float getScreenRatio();
+  abstract Static3D[] getCubitPositions(int size);
+  abstract Static4D[] getQuats();
+  abstract int getNumFaces();
+  abstract int getNumStickerTypes();
+  abstract int getNumCubitFaces();
+  abstract MeshBase createCubitMesh(int cubit);
+  abstract void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side);
+  abstract int getFaceColor(int cubit, int cubitface, int size);
+  abstract float returnMultiplier();
+  abstract float[] getRowChances();
+  abstract float getBasicStep();
+  abstract boolean shouldResetTextureMaps();
+
+  public abstract boolean isSolved();
+  public abstract Static3D[] getRotationAxis();
+  public abstract int getBasicAngle();
+  public abstract int computeRowFromOffset(float offset);
+  public abstract float returnRotationFactor(float offset);
+  public abstract String retObjectString();
+  public abstract int randomizeNewRotAxis(Random rnd, int oldRotAxis);
+  public abstract int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis);
+  }
diff --git a/src/main/java/org/distorted/objects/TwistyPyraminx.java b/src/main/java/org/distorted/objects/TwistyPyraminx.java
new file mode 100644
index 00000000..db244ad1
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistyPyraminx.java
@@ -0,0 +1,644 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.effect.VertexEffectMove;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.effect.VertexEffectSink;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.RubikSurfaceView;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyPyraminx extends TwistyObject
+{
+  private static final float SQ2 = (float)Math.sqrt(2);
+  private static final float SQ3 = (float)Math.sqrt(3);
+  private static final float SQ6 = (float)Math.sqrt(6);
+
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(         0,        1,       0 ),
+           new Static3D(         0,  -1.0f/3, 2*SQ2/3 ),
+           new Static3D(-SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 ),
+           new Static3D( SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 )
+         };
+
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(         0,      -1,       0 ),
+           new Static3D(         0,  1.0f/3,-2*SQ2/3 ),
+           new Static3D( SQ2*SQ3/3,  1.0f/3,   SQ2/3 ),
+           new Static3D(-SQ2*SQ3/3,  1.0f/3,   SQ2/3 )
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_GREEN , COLOR_YELLOW,
+           COLOR_BLUE  , COLOR_RED
+         };
+
+  // computed with res/raw/compute_quats.c
+  private static final Static4D[] QUATS = new Static4D[]
+         {
+           new Static4D(  0.0f,   0.0f,   0.0f,  1.0f),
+           new Static4D(  0.0f,  SQ3/2,   0.0f,  0.5f),
+           new Static4D( SQ2/2, -SQ3/6, -SQ6/6,  0.5f),
+           new Static4D(-SQ2/2, -SQ3/6, -SQ6/6,  0.5f),
+           new Static4D(  0.0f, -SQ3/6,  SQ6/3,  0.5f),
+           new Static4D(  0.0f,  SQ3/2,   0.0f, -0.5f),
+           new Static4D( SQ2/2, -SQ3/6, -SQ6/6, -0.5f),
+           new Static4D(-SQ2/2, -SQ3/6, -SQ6/6, -0.5f),
+           new Static4D(  0.0f, -SQ3/6,  SQ6/3, -0.5f),
+           new Static4D( SQ2/2, -SQ3/3,  SQ6/6,  0.0f),
+           new Static4D(  0.0f, -SQ3/3, -SQ6/3,  0.0f),
+           new Static4D(-SQ2/2, -SQ3/3,  SQ6/6,  0.0f)
+         };
+
+  private int[] mRotArray;
+  private static VertexEffectRotate[] ROTATION;
+
+  private static MeshBase mMesh =null;
+  private static MeshBase[] mMeshRotated = new MeshBase[ROT_AXIS.length];
+
+  static
+    {
+    Static3D center = new Static3D(0,0,0);
+    Static1D angle  = new Static1D(180.0f);
+
+    ROTATION = new VertexEffectRotate[ROT_AXIS.length];
+
+    for(int i = 0; i< ROT_AXIS.length; i++)
+      {
+      ROTATION[i] = new VertexEffectRotate( angle, ROT_AXIS[i], center);
+      mMeshRotated[i] = null;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyPyraminx(int size, Static4D quat, DistortedTexture texture,
+                 MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, 30, quat, texture, mesh, effects, moves, ObjectList.PYRA, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void emitRow(float x, float y, float z, float dx, float dy, float dz, int n, int rot, Static3D[] array, int index)
+    {
+    for(int i=0; i<n; i++)
+      {
+      mRotArray[i+index] = rot;
+      array[i+index] = new Static3D(x+0.5f,y+SQ2*SQ3/12,z+SQ3/6);
+      x += dx;
+      y += dy;
+      z += dz;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int emitLowermost(float x, float y, float z, int n, Static3D[] array)
+    {
+    int added = 0;
+
+    emitRow( x      +0.5f, y+SQ3*SQ2/9, z+ SQ3/18,  1.0f, 0,     0, n-1, 1, array, added);
+    added += (n-1);
+    emitRow( x    +1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9,  0.5f, 0, SQ3/2, n-1, 3, array, added);
+    added += (n-1);
+    emitRow( x+n-1-1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9, -0.5f, 0, SQ3/2, n-1, 2, array, added);
+    added += (n-1);
+
+    for(int i=n; i>=1; i--)
+      {
+      emitRow(x     , y, z      , 1,0,0, i  , -1, array, added);
+      added += i;
+      emitRow(x+0.5f, y, z+SQ3/6, 1,0,0, i-1,  0, array, added);
+      added += (i-1);
+      x += 0.5f;
+      y += 0.0f;
+      z += SQ3/2;
+      }
+
+    return added;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int emitUpper(float x, float y, float z, int n, Static3D[] array, int index)
+    {
+    if( n>1 )
+      {
+      emitRow( x           , y          , z        ,  1.0f, 0,     0, n-1, -1, array, index);
+      index += (n-1);
+      emitRow( x+0.5f      , y+SQ3*SQ2/9, z+SQ3/18 ,  1.0f, 0,     0, n-1,  1, array, index);
+      index += (n-1);
+      emitRow( x+0.5f      , y          , z+SQ3/2  ,  0.5f, 0, SQ3/2, n-1, -1, array, index);
+      index += (n-1);
+      emitRow( x    +1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9,  0.5f, 0, SQ3/2, n-1,  3, array, index);
+      index += (n-1);
+      emitRow( x+n-1       , y          , z        , -0.5f, 0, SQ3/2, n-1, -1, array, index);
+      index += (n-1);
+      emitRow( x+n-1-1.0f/3, y+SQ3*SQ2/9, z+2*SQ3/9, -0.5f, 0, SQ3/2, n-1,  2, array, index);
+      index += (n-1);
+      }
+    else
+      {
+      mRotArray[index] = -1;
+      array[index] = new Static3D(x+0.5f,y+SQ2*SQ3/12,z+SQ3/6);
+      index++;
+      }
+
+    return index;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// size^2 + 3*(size-1) in the lowermost layer, then 6*(size-2) in the next, 6*(size-3) in the next,
+// ... 6 in the forelast, 1 in the last = 4size^2 - 6size +4 (if size>1)
+
+  Static3D[] getCubitPositions(int size)
+    {
+    int numCubits = size>1 ? 4*size*size - 6*size +4 : 1;
+    Static3D[] tmp = new Static3D[numCubits];
+    mRotArray = new int[numCubits];
+
+    int currentIndex = emitLowermost( -0.5f*size, -(SQ2*SQ3/12)*size, -(SQ3/6)*size, size, tmp);
+
+    for(int i=size-1; i>=1; i--)
+      {
+      currentIndex = emitUpper( -0.5f*i, ((SQ2*SQ3)/12)*(3*size-4*i), -(SQ3/6)*i, i, tmp, currentIndex);
+      }
+
+    return tmp;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumStickerTypes()
+    {
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getBasicStep()
+    {
+    return SQ6/3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.82f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    boolean belongs = isOnFace(cubit, cubitface, 0 );
+    return belongs ? cubitface : NUM_FACES;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private MeshBase createStaticMesh(int cubit)
+    {
+    final float SQ2 = (float)Math.sqrt(2);
+    final float SQ3 = (float)Math.sqrt(3);
+    final float angleFaces = (float)((180/Math.PI)*(2*Math.asin(SQ3/3))); // angle between two faces of a tetrahedron
+    final int MESHES=4;
+
+    int size = getSize();
+    int association = 1;
+
+    float D = 0.0005f;
+    float E = SQ3/2 - 3*D*SQ2;
+    float F = 0.5f - D*SQ2*SQ3;
+    float[] bands;
+    int extraI, extraV;
+
+    float[] vertices = { -F,-E/3, +F,-E/3, 0.0f,2*E/3};
+
+    switch(size)
+      {
+      case 3 : bands = new float[] { 1.0f    ,-D,
+                                     1.0f  -D,-D*0.80f,
+                                     1.0f-2*D,-D*0.65f,
+                                     1.0f-4*D,+D*0.10f,
+                                     0.50f, 0.035f,
+                                     0.0f, 0.040f };
+                      extraI = 2;
+                      extraV = 2;
+                      break;
+      case 4 : bands = new float[] { 1.0f    ,-D,
+                                     1.0f-D*1.2f,-D*0.70f,
+                                     1.0f-3*D, -D*0.15f,
+                                     0.50f, 0.035f,
+                                     0.0f, 0.040f };
+                      extraI = 2;
+                      extraV = 2;
+                      break;
+      default: bands = new float[] { 1.0f    ,-D,
+                                     1.0f-D*1.2f,-D*0.70f,
+                                     1.0f-3*D, -D*0.15f,
+                                     0.50f, 0.035f,
+                                     0.0f, 0.040f };
+                      extraI = 2;
+                      extraV = 1;
+                      break;
+      }
+
+    MeshBase[] meshes = new MeshPolygon[MESHES];
+    meshes[0] = new MeshPolygon(vertices, bands, extraI,extraV);
+    meshes[0].setEffectAssociation(0,association,0);
+
+    for(int i=1; i<MESHES; i++)
+      {
+      association <<= 1;
+      meshes[i] = meshes[0].copy(true);
+      meshes[i].setEffectAssociation(0,association,0);
+      }
+
+    MeshBase result = new MeshJoined(meshes);
+
+    Static3D a0 = new Static3D(         0,        1,       0 );
+    Static3D a1 = new Static3D(         0,  -1.0f/3, 2*SQ2/3 );
+    Static3D a2 = new Static3D(-SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 );
+    Static3D a3 = new Static3D( SQ2*SQ3/3,  -1.0f/3,  -SQ2/3 );
+
+    float tetraHeight = SQ2*SQ3/3;
+    float d1 = (0.75f-2*SQ2*D)*tetraHeight;
+    float d2 =-0.06f*tetraHeight;
+    float d3 = 0.05f*tetraHeight;
+    float d4 = 0.70f*tetraHeight;
+    float d5 = 1.2f;
+
+    Static3D dCen0 = new Static3D( d1*a0.get0(), d1*a0.get1(), d1*a0.get2() );
+    Static3D dCen1 = new Static3D( d1*a1.get0(), d1*a1.get1(), d1*a1.get2() );
+    Static3D dCen2 = new Static3D( d1*a2.get0(), d1*a2.get1(), d1*a2.get2() );
+    Static3D dCen3 = new Static3D( d1*a3.get0(), d1*a3.get1(), d1*a3.get2() );
+
+    Static3D dVec0 = new Static3D( d2*a0.get0(), d2*a0.get1(), d2*a0.get2() );
+    Static3D dVec1 = new Static3D( d2*a1.get0(), d2*a1.get1(), d2*a1.get2() );
+    Static3D dVec2 = new Static3D( d2*a2.get0(), d2*a2.get1(), d2*a2.get2() );
+    Static3D dVec3 = new Static3D( d2*a3.get0(), d2*a3.get1(), d2*a3.get2() );
+
+    Static4D dReg  = new Static4D(0,0,0,d3);
+    Static1D dRad  = new Static1D(1);
+    Static3D center= new Static3D(0,0,0);
+    Static4D sReg  = new Static4D(0,0,0,d4);
+    Static1D sink  = new Static1D(d5);
+
+    Static1D angle  = new Static1D(angleFaces);
+    Static3D axis1  = new Static3D(  -1, 0,      0);
+    Static3D axis2  = new Static3D(0.5f, 0, -SQ3/2);
+    Static3D axis3  = new Static3D(0.5f, 0, +SQ3/2);
+    Static3D center1= new Static3D(0,-SQ3*SQ2/12,-SQ3/6);
+    Static3D center2= new Static3D(0,-SQ3*SQ2/12,+SQ3/3);
+
+    VertexEffectRotate  effect1 = new VertexEffectRotate( new Static1D(90), new Static3D(1,0,0), center );
+    VertexEffectMove    effect2 = new VertexEffectMove  ( new Static3D(0,-SQ3*SQ2/12,0) );
+    VertexEffectRotate  effect3 = new VertexEffectRotate( new Static1D(180), new Static3D(0,0,1), center1 );
+    VertexEffectRotate  effect4 = new VertexEffectRotate( angle, axis1, center1 );
+    VertexEffectRotate  effect5 = new VertexEffectRotate( angle, axis2, center2 );
+    VertexEffectRotate  effect6 = new VertexEffectRotate( angle, axis3, center2 );
+
+    VertexEffectDeform  effect7 = new VertexEffectDeform(dVec0, dRad, dCen0, dReg);
+    VertexEffectDeform  effect8 = new VertexEffectDeform(dVec1, dRad, dCen1, dReg);
+    VertexEffectDeform  effect9 = new VertexEffectDeform(dVec2, dRad, dCen2, dReg);
+    VertexEffectDeform  effect10= new VertexEffectDeform(dVec3, dRad, dCen3, dReg);
+
+    VertexEffectSink effect11= new VertexEffectSink(sink,center, sReg);
+
+    effect3.setMeshAssociation(14,-1);  // apply to mesh[1], [2] and [3]
+    effect4.setMeshAssociation( 2,-1);  // apply only to mesh[1]
+    effect5.setMeshAssociation( 4,-1);  // apply only to mesh[2]
+    effect6.setMeshAssociation( 8,-1);  // apply only to mesh[3]
+
+    result.apply(effect1);
+    result.apply(effect2);
+    result.apply(effect3);
+    result.apply(effect4);
+    result.apply(effect5);
+    result.apply(effect6);
+
+    result.apply(effect7);
+    result.apply(effect8);
+    result.apply(effect9);
+    result.apply(effect10);
+
+    result.apply(effect11);
+
+    if( mRotArray[cubit]>=0 )
+      {
+      result.apply( ROTATION[mRotArray[cubit]] );
+      }
+
+    result.mergeEffComponents();
+
+    return result;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    int kind = mRotArray[cubit];
+
+    if( kind>=0 )
+      {
+      if( mMeshRotated[kind]==null ) mMeshRotated[kind] = createStaticMesh(cubit);
+      return mMeshRotated[kind].copy(true);
+      }
+    else
+      {
+      if( mMesh==null ) mMesh = createStaticMesh(cubit);
+      return mMesh.copy(true);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    float STROKE = 0.044f*side;
+    float OFF = STROKE/2 -1;
+    float OFF2 = 0.5f*side + OFF;
+    float HEIGHT = side - OFF;
+    float RADIUS = side/12.0f;
+    float ARC1_H = 0.2f*side;
+    float ARC1_W = side*0.5f;
+    float ARC2_W = 0.153f*side;
+    float ARC2_H = 0.905f*side;
+    float ARC3_W = side-ARC2_W;
+
+    float M = SQ3/2;
+    float D = (M/2 - 0.51f)*side;
+
+    paint.setAntiAlias(true);
+    paint.setStrokeWidth(STROKE);
+    paint.setColor(FACE_COLORS[face]);
+    paint.setStyle(Paint.Style.FILL);
+
+    canvas.drawRect(left,top,left+side,top+side,paint);
+
+    paint.setColor(INTERIOR_COLOR);
+    paint.setStyle(Paint.Style.STROKE);
+
+    canvas.drawLine(           left, M*HEIGHT+D,  side       +left, M*HEIGHT+D, paint);
+    canvas.drawLine(      OFF +left, M*side  +D,       OFF2  +left,          D, paint);
+    canvas.drawLine((side-OFF)+left, M*side  +D, (side-OFF2) +left,          D, paint);
+
+    canvas.drawArc( ARC1_W-RADIUS+left, M*(ARC1_H-RADIUS)+D, ARC1_W+RADIUS+left, M*(ARC1_H+RADIUS)+D, 225, 90, false, paint);
+    canvas.drawArc( ARC2_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC2_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 105, 90, false, paint);
+    canvas.drawArc( ARC3_W-RADIUS+left, M*(ARC2_H-RADIUS)+D, ARC3_W+RADIUS+left, M*(ARC2_H+RADIUS)+D, 345, 90, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// SQ6/3 = height of the tetrahedron
+
+  float returnMultiplier()
+    {
+    return getSize()/(SQ6/3);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    int size = getSize();
+    int total = size*(size+1)/2;
+    float running=0.0f;
+    float[] chances = new float[size];
+
+    for(int i=0; i<size; i++)
+      {
+      running += (size-i);
+      chances[i] = running / total;
+      }
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return (int)(getSize()*offset/(SQ6/3));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    int size = getSize();
+    int row  = (int)(size*offset/(SQ3/2));
+
+    return ((float)size)/(size-row);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-1);
+      return (newVector>=oldRotAxis ? newVector+1 : newVector);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    float rowFloat = rnd.nextFloat();
+
+    for(int row=0; row<mRowChances.length; row++)
+      {
+      if( rowFloat<=mRowChances[row] ) return row;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isSolved()
+    {
+    int index = CUBITS[0].mQuatIndex;
+
+    for(int i=1; i<NUM_CUBITS; i++)
+      {
+      if( !thereIsNoVisibleDifference(CUBITS[i], index) ) return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return if the Cubit, when rotated with its own mQuatScramble, would have looked any different
+// then if it were rotated by quaternion 'quat'.
+// No it is not so simple as the quats need to be the same - imagine a 4x4x4 cube where the two
+// middle squares get interchanged. No visible difference!
+//
+// So: this is true iff the cubit
+// a) is a corner or edge and the quaternions are the same
+// b) is inside one of the faces and after rotations by both quats it ends up on the same face.
+
+  private boolean thereIsNoVisibleDifference(Cubit cubit, int quatIndex)
+    {
+    if ( cubit.mQuatIndex == quatIndex ) return true;
+
+    int belongsToHowManyFaces = 0;
+    int size = getSize()-1;
+    float row;
+    final float MAX_ERROR = 0.01f;
+
+    for(int i=0; i<NUM_AXIS; i++)
+      {
+      row = cubit.mRotationRow[i];
+      if( (row     <MAX_ERROR && row     >-MAX_ERROR) ||
+          (row-size<MAX_ERROR && row-size>-MAX_ERROR)  ) belongsToHowManyFaces++;
+      }
+
+    switch(belongsToHowManyFaces)
+      {
+      case 0 : return true ;  // 'inside' cubit that does not lie on any face
+      case 1 :                // cubit that lies inside one of the faces
+               Static3D orig = cubit.getOrigPosition();
+               Static4D quat1 = QUATS[quatIndex];
+               Static4D quat2 = QUATS[cubit.mQuatIndex];
+
+               Static4D cubitCenter = new Static4D( orig.get0(), orig.get1(), orig.get2(), 0);
+               Static4D rotated1 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat1 );
+               Static4D rotated2 = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat2 );
+
+               float row1, row2, row3, row4;
+               float ax,ay,az;
+               Static3D axis;
+               float x1 = rotated1.get0();
+               float y1 = rotated1.get1();
+               float z1 = rotated1.get2();
+               float x2 = rotated2.get0();
+               float y2 = rotated2.get1();
+               float z2 = rotated2.get2();
+
+               for(int i=0; i<NUM_AXIS; i++)
+                 {
+                 axis = ROTATION_AXIS[i];
+                 ax = axis.get0();
+                 ay = axis.get1();
+                 az = axis.get2();
+
+                 row1 = ((x1*ax + y1*ay + z1*az) - mStart) / mStep;
+                 row2 = ((x2*ax + y2*ay + z2*az) - mStart) / mStep;
+                 row3 = row1 - size;
+                 row4 = row2 - size;
+
+                 if( (row1<MAX_ERROR && row1>-MAX_ERROR && row2<MAX_ERROR && row2>-MAX_ERROR) ||
+                     (row3<MAX_ERROR && row3>-MAX_ERROR && row4<MAX_ERROR && row4>-MAX_ERROR)  )
+                   {
+                   return true;
+                   }
+                 }
+               return false;
+
+      default: return false;  // edge or corner
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// only needed for solvers - there are no Pyraminx solvers ATM)
+
+  public String retObjectString()
+    {
+    return "";
+    }
+}
diff --git a/src/main/java/org/distorted/objects/TwistySkewb.java b/src/main/java/org/distorted/objects/TwistySkewb.java
new file mode 100644
index 00000000..5ea8ed18
--- /dev/null
+++ b/src/main/java/org/distorted/objects/TwistySkewb.java
@@ -0,0 +1,709 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.effect.VertexEffectMove;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.effect.VertexEffectScale;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.mesh.MeshTriangle;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.RubikSurfaceView;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistySkewb extends TwistyObject
+{
+  private static final float SQ2 = (float)Math.sqrt(2);
+  private static final float SQ3 = (float)Math.sqrt(3);
+
+  private static final int FACES_PER_CUBIT =6;
+
+  // the four rotation axis of a RubikSkewb. Must be normalized.
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(+SQ3/3,+SQ3/3,+SQ3/3),
+           new Static3D(+SQ3/3,+SQ3/3,-SQ3/3),
+           new Static3D(+SQ3/3,-SQ3/3,+SQ3/3),
+           new Static3D(+SQ3/3,-SQ3/3,-SQ3/3)
+         };
+
+  // the six axis that determine the faces
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(1,0,0), new Static3D(-1,0,0),
+           new Static3D(0,1,0), new Static3D(0,-1,0),
+           new Static3D(0,0,1), new Static3D(0,0,-1)
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_YELLOW, COLOR_WHITE,
+           COLOR_BLUE  , COLOR_GREEN,
+           COLOR_RED   , COLOR_BROWN
+         };
+
+  // All legal rotation quats of a RubikSkewb
+  private static final Static4D[] QUATS = new Static4D[]
+         {
+           new Static4D(  0.0f,  0.0f,  0.0f,  1.0f ),
+           new Static4D(  1.0f,  0.0f,  0.0f,  0.0f ),
+           new Static4D(  0.0f,  1.0f,  0.0f,  0.0f ),
+           new Static4D(  0.0f,  0.0f,  1.0f,  0.0f ),
+
+           new Static4D(  0.5f,  0.5f,  0.5f,  0.5f ),
+           new Static4D(  0.5f,  0.5f,  0.5f, -0.5f ),
+           new Static4D(  0.5f,  0.5f, -0.5f,  0.5f ),
+           new Static4D(  0.5f,  0.5f, -0.5f, -0.5f ),
+           new Static4D(  0.5f, -0.5f,  0.5f,  0.5f ),
+           new Static4D(  0.5f, -0.5f,  0.5f, -0.5f ),
+           new Static4D(  0.5f, -0.5f, -0.5f,  0.5f ),
+           new Static4D(  0.5f, -0.5f, -0.5f, -0.5f )
+         };
+
+  private static final float DIST_CORNER = 0.50f;
+  private static final float DIST_CENTER = 0.49f;
+
+  // centers of the 8 corners + 6 sides ( i.e. of the all 14 cubits)
+  private static final Static3D[] CENTERS = new Static3D[]
+         {
+           new Static3D( DIST_CORNER, DIST_CORNER, DIST_CORNER ),
+           new Static3D( DIST_CORNER, DIST_CORNER,-DIST_CORNER ),
+           new Static3D( DIST_CORNER,-DIST_CORNER, DIST_CORNER ),
+           new Static3D( DIST_CORNER,-DIST_CORNER,-DIST_CORNER ),
+           new Static3D(-DIST_CORNER, DIST_CORNER, DIST_CORNER ),
+           new Static3D(-DIST_CORNER, DIST_CORNER,-DIST_CORNER ),
+           new Static3D(-DIST_CORNER,-DIST_CORNER, DIST_CORNER ),
+           new Static3D(-DIST_CORNER,-DIST_CORNER,-DIST_CORNER ),
+
+           new Static3D( DIST_CENTER,        0.0f,        0.0f ),
+           new Static3D(-DIST_CENTER,        0.0f,        0.0f ),
+           new Static3D(        0.0f, DIST_CENTER,        0.0f ),
+           new Static3D(        0.0f,-DIST_CENTER,        0.0f ),
+           new Static3D(        0.0f,        0.0f, DIST_CENTER ),
+           new Static3D(        0.0f,        0.0f,-DIST_CENTER ),
+         };
+
+  // Colors of the faces of cubits. Each cubit, even the face pyramid, has 6 faces
+  // (the face has one extra 'fake' face so that everything would have the same number)
+  private static final int[][] mFaceMap = new int[][]
+         {
+           { 4,2,0, 12,12,12 },
+           { 2,5,0, 12,12,12 },
+           { 3,4,0, 12,12,12 },
+           { 5,3,0, 12,12,12 },
+           { 1,2,4, 12,12,12 },
+           { 5,2,1, 12,12,12 },
+           { 4,3,1, 12,12,12 },
+           { 1,3,5, 12,12,12 },
+
+           { 6 , 12,12,12,12,12 },
+           { 7 , 12,12,12,12,12 },
+           { 8 , 12,12,12,12,12 },
+           { 9 , 12,12,12,12,12 },
+           { 10, 12,12,12,12,12 },
+           { 11, 12,12,12,12,12 },
+         };
+
+  private static MeshBase mCornerMesh, mFaceMesh;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistySkewb(int size, Static4D quat, DistortedTexture texture,
+              MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, 60, quat, texture, mesh, effects, moves, ObjectList.SKEW, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createCornerMesh()
+    {
+    float D = 0.02f;
+    float E = 0.5f;
+    float F = SQ2/2;
+
+    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-2*D, D*0.25f,
+                       1.0f-4*D, D*0.35f,
+                       1.0f-8*D, D*0.6f,
+                       0.60f   , D*1.0f,
+                       0.30f   , D*1.375f,
+                       0.0f    , D*1.4f };
+
+    MeshBase[] meshes = new MeshBase[FACES_PER_CUBIT];
+
+    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
+    meshes[0].setEffectAssociation(0,1,0);
+    meshes[1] = meshes[0].copy(true);
+    meshes[1].setEffectAssociation(0,2,0);
+    meshes[2] = meshes[0].copy(true);
+    meshes[2].setEffectAssociation(0,4,0);
+
+    float[] vertices1 = { 0,0, F,0, F/2,(SQ3/2)*F };
+    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
+
+    meshes[3] = new MeshPolygon(vertices1,bands1,1,5);
+    meshes[3].setEffectAssociation(0,8,0);
+    meshes[4] = meshes[3].copy(true);
+    meshes[4].setEffectAssociation(0,16,0);
+    meshes[5] = meshes[3].copy(true);
+    meshes[5].setEffectAssociation(0,32,0);
+
+    mCornerMesh = new MeshJoined(meshes);
+
+    Static3D axisX  = new Static3D(1,0,0);
+    Static3D axisY  = new Static3D(0,1,0);
+    Static3D axis0  = new Static3D(-SQ2/2,0,SQ2/2);
+    Static3D axis1  = new Static3D(+SQ3/3,+SQ3/3,+SQ3/3);
+    Static1D angle1 = new Static1D(+90);
+    Static1D angle2 = new Static1D(-90);
+    Static1D angle3 = new Static1D(-15);
+    Static1D angle4 = new Static1D((float)((180.0f/Math.PI)*Math.acos(SQ3/3)));
+    Static1D angle5 = new Static1D(120);
+    Static1D angle6 = new Static1D(240);
+    Static3D center1= new Static3D(0,0,0);
+    Static3D center2= new Static3D(-0.5f,-0.5f,-0.5f);
+    Static3D move1  = new Static3D(-E/4,-E/4,0);
+    Static3D move2  = new Static3D(-0.5f,-0.5f,-0.5f);
+
+    float d0 =-0.04f;
+    float d1 = 0.04f;
+    float r0 = 0.15f;
+    float r1 = 0.10f;
+
+    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
+    Static3D vec1   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(-SQ3/3));
+    Static3D vec2   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(-SQ3/3));
+    Static3D vec3   = new Static3D(d1*(-SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
+
+    Static1D radius = new Static1D(0.5f);
+
+    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
+    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
+    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
+    Static3D cent3  = new Static3D( 0.0f, 0.0f,-0.5f);
+
+    Static4D reg0   = new Static4D(0,0,0,r0);
+    Static4D reg1   = new Static4D(0,0,0,r1);
+
+    VertexEffectMove   effect0 = new VertexEffectMove(move1);
+    VertexEffectScale  effect1 = new VertexEffectScale(new Static3D(1,1,-1));
+    VertexEffectRotate effect2 = new VertexEffectRotate(angle1,axisX,center1);
+    VertexEffectRotate effect3 = new VertexEffectRotate(angle2,axisY,center1);
+    VertexEffectMove   effect4 = new VertexEffectMove(move2);
+    VertexEffectRotate effect5 = new VertexEffectRotate(angle1,axisX,center2);
+    VertexEffectRotate effect6 = new VertexEffectRotate(angle3,axisY,center2);
+    VertexEffectRotate effect7 = new VertexEffectRotate(angle4,axis0,center2);
+    VertexEffectRotate effect8 = new VertexEffectRotate(angle5,axis1,center2);
+    VertexEffectRotate effect9 = new VertexEffectRotate(angle6,axis1,center2);
+
+    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
+    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
+    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
+    VertexEffectDeform effect13= new VertexEffectDeform(vec3,radius,cent3,reg1);
+
+    effect0.setMeshAssociation( 7,-1);  // meshes 0,1,2
+    effect1.setMeshAssociation( 6,-1);  // meshes 1,2
+    effect2.setMeshAssociation( 2,-1);  // mesh 1
+    effect3.setMeshAssociation( 4,-1);  // mesh 2
+    effect4.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect5.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect6.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect7.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect8.setMeshAssociation(16,-1);  // mesh 4
+    effect9.setMeshAssociation(32,-1);  // mesh 5
+
+    effect10.setMeshAssociation(63,-1); // all meshes
+    effect11.setMeshAssociation(63,-1); // all meshes
+    effect12.setMeshAssociation(63,-1); // all meshes
+    effect13.setMeshAssociation(63,-1); // all meshes
+
+    mCornerMesh.apply(effect0);
+    mCornerMesh.apply(effect1);
+    mCornerMesh.apply(effect2);
+    mCornerMesh.apply(effect3);
+    mCornerMesh.apply(effect4);
+    mCornerMesh.apply(effect5);
+    mCornerMesh.apply(effect6);
+    mCornerMesh.apply(effect7);
+    mCornerMesh.apply(effect8);
+    mCornerMesh.apply(effect9);
+
+    mCornerMesh.apply(effect10);
+    mCornerMesh.apply(effect11);
+    mCornerMesh.apply(effect12);
+    mCornerMesh.apply(effect13);
+
+    mCornerMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createFaceMesh()
+    {
+    int association = 1;
+
+    float D = 0.03f;
+    float E = SQ2/4;
+    float[] vertices0 = { -E,-E, +E,-E, +E,+E, -E,+E };
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-D/2, D*0.30f,
+                       1.0f- D , D*0.50f,
+                       1.0f-2*D, D*0.80f,
+                       0.60f   , D*1.40f,
+                       0.30f   , D*1.60f,
+                       0.0f    , D*1.70f };
+
+    MeshBase[] meshes = new MeshBase[FACES_PER_CUBIT];
+    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
+    meshes[0].setEffectAssociation(0,association,0);
+
+    association <<= 1;
+
+    float[] vertices1 = { -E,-SQ3*E, +E,-SQ3*E, 0,0 };
+    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
+
+    meshes[1] = new MeshPolygon(vertices1,bands1,0,0);
+    meshes[1].setEffectAssociation(0,association,0);
+
+    for(int i=2; i<FACES_PER_CUBIT-1; i++)
+      {
+      association <<= 1;
+      meshes[i] = meshes[1].copy(true);
+      meshes[i].setEffectAssociation(0,association,0);
+      }
+
+    association <<= 1;
+    meshes[FACES_PER_CUBIT-1] = new MeshTriangle(1);                  // empty triangle so that
+    meshes[FACES_PER_CUBIT-1].setEffectAssociation(0,association,0);  // all cubits have 6 faces
+
+    mFaceMesh = new MeshJoined(meshes);
+
+    Static3D center = new Static3D(0,0,0);
+    Static3D axis1   = new Static3D(1,0,0);
+    Static3D axis2   = new Static3D(0,0,1);
+    float angle = -(float)((180.0f/Math.PI)*Math.acos(SQ3/3));
+
+    float f = 0.05f;
+    float r = 0.10f;
+    float d = 0.5f;
+    float e = +D*0.6f;
+    Static3D vector0 = new Static3D(-f, 0, 0);
+    Static3D vector1 = new Static3D( 0,+f, 0);
+    Static3D vector2 = new Static3D(+f, 0, 0);
+    Static3D vector3 = new Static3D( 0,-f, 0);
+    Static1D radius  = new Static1D(1.0f);
+    Static4D region  = new Static4D(0,0,0,r);
+    Static3D center0 = new Static3D(+d, 0, e);
+    Static3D center1 = new Static3D( 0,-d, e);
+    Static3D center2 = new Static3D(-d, 0, e);
+    Static3D center3 = new Static3D( 0,+d, e);
+
+    VertexEffectRotate effect0 = new VertexEffectRotate( new Static1D(angle), axis1, center);
+    VertexEffectRotate effect1 = new VertexEffectRotate( new Static1D(  135), axis2, center);
+    VertexEffectRotate effect2 = new VertexEffectRotate( new Static1D(   45), axis2, center);
+    VertexEffectRotate effect3 = new VertexEffectRotate( new Static1D(  -45), axis2, center);
+    VertexEffectRotate effect4 = new VertexEffectRotate( new Static1D( -135), axis2, center);
+    VertexEffectMove   effect5 = new VertexEffectMove( new Static3D(0,0,-0.5f) );
+    VertexEffectDeform effect6 = new VertexEffectDeform(vector0,radius,center0,region);
+    VertexEffectDeform effect7 = new VertexEffectDeform(vector1,radius,center1,region);
+    VertexEffectDeform effect8 = new VertexEffectDeform(vector2,radius,center2,region);
+    VertexEffectDeform effect9 = new VertexEffectDeform(vector3,radius,center3,region);
+    VertexEffectScale  effect10= new VertexEffectScale(0.01f);
+
+    effect0.setMeshAssociation(30,-1);  // meshes 1,2,3,4
+    effect1.setMeshAssociation( 2,-1);  // mesh 1
+    effect2.setMeshAssociation( 5,-1);  // meshes 0,2
+    effect3.setMeshAssociation( 8,-1);  // mesh 3
+    effect4.setMeshAssociation(16,-1);  // mesh 4
+    effect5.setMeshAssociation(30,-1);  // meshes 1,2,3,4
+    effect6.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
+    effect7.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
+    effect8.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
+    effect9.setMeshAssociation(31,-1);  // meshes 0,1,2,3,4
+    effect10.setMeshAssociation(32,-1); // mesh 5
+
+    mFaceMesh.apply(effect0);
+    mFaceMesh.apply(effect1);
+    mFaceMesh.apply(effect2);
+    mFaceMesh.apply(effect3);
+    mFaceMesh.apply(effect4);
+    mFaceMesh.apply(effect5);
+    mFaceMesh.apply(effect6);
+    mFaceMesh.apply(effect7);
+    mFaceMesh.apply(effect8);
+    mFaceMesh.apply(effect9);
+    mFaceMesh.apply(effect10);
+
+    mFaceMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Each face has two types of a texture: the central square and the triangle in the corner.
+
+  int getNumStickerTypes()
+    {
+    return 2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getBasicStep()
+    {
+    return SQ3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return FACES_PER_CUBIT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static3D[] getCubitPositions(int size)
+    {
+    return CENTERS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Static4D getQuat(int cubit)
+    {
+    switch(cubit)
+      {
+      case  0: return QUATS[0];                          //  unit quat
+      case  1: return new Static4D( SQ2/2,0,0,SQ2/2);    //  90 along X
+      case  2: return new Static4D(-SQ2/2,0,0,SQ2/2);    // -90 along X
+      case  3: return QUATS[1];                          // 180 along X
+      case  4: return new Static4D(0, SQ2/2,0,SQ2/2);    //  90 along Y
+      case  5: return QUATS[2];                          // 180 along Y
+      case  6: return QUATS[3];                          // 180 along Z
+      case  7: return new Static4D(SQ2/2,0,-SQ2/2,0);    // 180 along (SQ2/2,0,-SQ2/2)
+      case  8: return new Static4D(0,-SQ2/2,0,SQ2/2);    // -90 along Y
+      case  9: return new Static4D(0, SQ2/2,0,SQ2/2);    //  90 along Y
+      case 10: return new Static4D( SQ2/2,0,0,SQ2/2);    //  90 along X
+      case 11: return new Static4D(-SQ2/2,0,0,SQ2/2);    // -90 along X
+      case 12: return QUATS[0];                          //  unit quaternion
+      case 13: return QUATS[1];                          // 180 along X
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    MeshBase mesh;
+
+    if( cubit<8 )
+      {
+      if( mCornerMesh==null ) createCornerMesh();
+      mesh = mCornerMesh.copy(true);
+      }
+    else
+      {
+      if( mFaceMesh==null ) createFaceMesh();
+      mesh = mFaceMesh.copy(true);
+      }
+
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( getQuat(cubit), new Static3D(0,0,0) );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    return mFaceMap[cubit][cubitface];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    int COLORS = FACE_COLORS.length;
+
+    if( face<COLORS )
+      {
+      float STROKE = 0.035f*side;
+      float L= left+0.125f*side;
+      float H= 0.375f*side;
+      float LEN = 0.5f*side;
+
+      paint.setAntiAlias(true);
+      paint.setStrokeWidth(STROKE);
+      paint.setColor(FACE_COLORS[face]);
+      paint.setStyle(Paint.Style.FILL);
+
+      canvas.drawRect(left,top,left+side,top+side,paint);
+
+      paint.setColor(INTERIOR_COLOR);
+      paint.setStyle(Paint.Style.STROKE);
+
+      canvas.drawLine( L    , H,  L+LEN, H    , paint);
+      canvas.drawLine( L    , H,  L+LEN, H+LEN, paint);
+      canvas.drawLine( L+LEN, H,  L+LEN, H+LEN, paint);
+
+      float S1 = 0.125f*side;
+      float S2 = 0.070f*side;
+      float X  = 0.7f*S2;
+
+      float LA = left+0.625f*side;
+      float RA = left+0.125f*side;
+      float TA = 0.375f*side;
+      float BA = 0.875f*side;
+
+      canvas.drawArc( LA-S1, TA     , LA     , TA+S1, 270, 90, false, paint);
+      canvas.drawArc( RA+X , TA     , RA+X+S2, TA+S2, 135,135, false, paint);
+      canvas.drawArc( LA-S2, BA-X-S2, LA     , BA-X ,   0,135, false, paint);
+      }
+    else
+      {
+      final float R = (SQ2/2)*side*0.10f;
+      final float M = side*(0.5f-SQ2/4+0.018f);
+
+      paint.setColor(FACE_COLORS[face-COLORS]);
+      paint.setStyle(Paint.Style.FILL);
+      canvas.drawRoundRect( left+M, top+M, left+side-M, top+side-M, R, R, paint);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 2.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    float[] chances = new float[2];
+
+    chances[0] = 0.5f;
+    chances[1] = 1.0f;
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return offset<0.25f ? 0:1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-1);
+      return (newVector>=oldRotAxis ? newVector+1 : newVector);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    float rowFloat = rnd.nextFloat();
+
+    for(int row=0; row<mRowChances.length; row++)
+      {
+      if( rowFloat<=mRowChances[row] ) return row;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// remember about the double cover or unit quaternions!
+
+  private int mulQuat(int q1, int q2)
+    {
+    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
+
+    float rX = result.get0();
+    float rY = result.get1();
+    float rZ = result.get2();
+    float rW = result.get3();
+
+    final float MAX_ERROR = 0.1f;
+    float dX,dY,dZ,dW;
+
+    for(int i=0; i<QUATS.length; i++)
+      {
+      dX = QUATS[i].get0() - rX;
+      dY = QUATS[i].get1() - rY;
+      dZ = QUATS[i].get2() - rZ;
+      dW = QUATS[i].get3() - rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+
+      dX = QUATS[i].get0() + rX;
+      dY = QUATS[i].get1() + rY;
+      dZ = QUATS[i].get2() + rZ;
+      dW = QUATS[i].get3() + rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// The Skewb is solved if and only if:
+//
+// 1) all of its corner cubits are rotated with the same quat
+// 2) all its face cubits are rotated with the same quat like the corner ones,
+//    and optionally they also might be upside down.
+//
+// i.e.
+// cubits [ 8] and [ 9] - might be extra QUAT[1]
+// cubits [10] and [11] - might be extra QUAT[2]
+// cubits [12] and [13] - might be extra QUAT[3]
+
+  public boolean isSolved()
+    {
+    int q = CUBITS[0].mQuatIndex;
+
+    if ( CUBITS[1].mQuatIndex == q &&
+         CUBITS[2].mQuatIndex == q &&
+         CUBITS[3].mQuatIndex == q &&
+         CUBITS[4].mQuatIndex == q &&
+         CUBITS[5].mQuatIndex == q &&
+         CUBITS[6].mQuatIndex == q &&
+         CUBITS[7].mQuatIndex == q  )
+      {
+      int q1 = mulQuat(q,1);
+      int q2 = mulQuat(q,2);
+      int q3 = mulQuat(q,3);
+
+      return (CUBITS[ 8].mQuatIndex == q || CUBITS[ 8].mQuatIndex == q1) &&
+             (CUBITS[ 9].mQuatIndex == q || CUBITS[ 9].mQuatIndex == q1) &&
+             (CUBITS[10].mQuatIndex == q || CUBITS[10].mQuatIndex == q2) &&
+             (CUBITS[11].mQuatIndex == q || CUBITS[11].mQuatIndex == q2) &&
+             (CUBITS[12].mQuatIndex == q || CUBITS[12].mQuatIndex == q3) &&
+             (CUBITS[13].mQuatIndex == q || CUBITS[13].mQuatIndex == q3)  ;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// only needed for solvers - there are no Skewb solvers ATM)
+
+  public String retObjectString()
+    {
+    return "";
+    }
+
+}
diff --git a/src/main/java/org/distorted/patterns/RubikPatternList.java b/src/main/java/org/distorted/patterns/RubikPatternList.java
index 0ee67e37..c20c9b5c 100644
--- a/src/main/java/org/distorted/patterns/RubikPatternList.java
+++ b/src/main/java/org/distorted/patterns/RubikPatternList.java
@@ -19,23 +19,23 @@
 
 package org.distorted.patterns;
 
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public enum RubikPatternList
   {
-  CUBE2 (RubikObjectList.CUBE, 2, RubikPatternCube2.patterns),
-  CUBE3 (RubikObjectList.CUBE, 3, RubikPatternCube3.patterns),
-  CUBE4 (RubikObjectList.CUBE, 4, RubikPatternCube4.patterns),
-  CUBE5 (RubikObjectList.CUBE, 5, RubikPatternCube5.patterns),
-  PYRA3 (RubikObjectList.PYRA, 3, RubikPatternPyraminx3.patterns),
-  PYRA4 (RubikObjectList.PYRA, 4, RubikPatternPyraminx4.patterns),
-  PYRA5 (RubikObjectList.PYRA, 5, RubikPatternPyraminx5.patterns),
+  CUBE2 (ObjectList.CUBE, 2, RubikPatternCube2.patterns),
+  CUBE3 (ObjectList.CUBE, 3, RubikPatternCube3.patterns),
+  CUBE4 (ObjectList.CUBE, 4, RubikPatternCube4.patterns),
+  CUBE5 (ObjectList.CUBE, 5, RubikPatternCube5.patterns),
+  PYRA3 (ObjectList.PYRA, 3, RubikPatternPyraminx3.patterns),
+  PYRA4 (ObjectList.PYRA, 4, RubikPatternPyraminx4.patterns),
+  PYRA5 (ObjectList.PYRA, 5, RubikPatternPyraminx5.patterns),
   ;
 
   public static final int NUM_OBJECTS = values().length;
-  private RubikObjectList mObject;
+  private ObjectList mObject;
   private int mSize;
   private String[][] mPatterns;
 
@@ -61,7 +61,7 @@ public enum RubikPatternList
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  RubikPatternList(RubikObjectList object, int size, String[][] patterns)
+  RubikPatternList(ObjectList object, int size, String[][] patterns)
     {
     mObject   = object;
     mSize     = size;
@@ -87,7 +87,7 @@ public enum RubikPatternList
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public static RubikObjectList getObject(int ordinal)
+  public static ObjectList getObject(int ordinal)
     {
     return objects[ordinal].mObject;
     }
diff --git a/src/main/java/org/distorted/scores/RubikScores.java b/src/main/java/org/distorted/scores/RubikScores.java
index 3e022a98..45c01fcc 100644
--- a/src/main/java/org/distorted/scores/RubikScores.java
+++ b/src/main/java/org/distorted/scores/RubikScores.java
@@ -23,13 +23,13 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.telephony.TelephonyManager;
 
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 
 import java.util.UUID;
 
-import static org.distorted.objects.RubikObjectList.MAX_NUM_OBJECTS;
-import static org.distorted.objects.RubikObjectList.NUM_OBJECTS;
-import static org.distorted.objects.RubikObjectList.MAX_LEVEL;
+import static org.distorted.objects.ObjectList.MAX_NUM_OBJECTS;
+import static org.distorted.objects.ObjectList.NUM_OBJECTS;
+import static org.distorted.objects.ObjectList.MAX_LEVEL;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // hold my own scores, and some other statistics.
@@ -97,7 +97,7 @@ public class RubikScores
 
   private String getUnsubmittedList(int mode)
     {
-    RubikObjectList list;
+    ObjectList list;
     StringBuilder builder = new StringBuilder();
     boolean first = true;
     int[] sizes;
@@ -105,7 +105,7 @@ public class RubikScores
 
     for(int object=0; object<NUM_OBJECTS; object++)
       {
-      list = RubikObjectList.getObject(object);
+      list = ObjectList.getObject(object);
       sizes = list.getSizes();
       length = sizes.length;
 
@@ -156,7 +156,7 @@ public class RubikScores
   public void savePreferences(SharedPreferences.Editor editor)
     {
     StringBuilder builder = new StringBuilder();
-    RubikObjectList list;
+    ObjectList list;
     int[] sizes;
     int length;
 
@@ -166,7 +166,7 @@ public class RubikScores
 
       for(int object=0; object<NUM_OBJECTS; object++)
         {
-        list = RubikObjectList.getObject(object);
+        list = ObjectList.getObject(object);
         sizes = list.getSizes();
         length = sizes.length;
 
@@ -227,11 +227,11 @@ public class RubikScores
           timeStr = subStr.substring(equals+1,comma);
           submStr = subStr.substring(comma+1);
 
-          object = RubikObjectList.getOrdinal(nameStr);
+          object = ObjectList.getOrdinal(nameStr);
 
           if( object>=0 && object< NUM_OBJECTS )
             {
-            sizeIndex = RubikObjectList.getSizeIndex(object,Integer.parseInt(sizeStr));
+            sizeIndex = ObjectList.getSizeIndex(object,Integer.parseInt(sizeStr));
             time = Long.parseLong(timeStr);
             subm = Integer.parseInt(submStr);
 
@@ -266,7 +266,7 @@ public class RubikScores
 
   public boolean setRecord(int object, int size, int level, long timeTaken)
     {
-    int maxsize = RubikObjectList.getObject(object).getSizes().length;
+    int maxsize = ObjectList.getObject(object).getSizes().length;
 
     if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=1 && level<=MAX_LEVEL )
       {
@@ -352,7 +352,7 @@ public class RubikScores
 
   public boolean isSolved(int object, int size, int level)
     {
-    int maxsize = RubikObjectList.getObject(object).getSizes().length;
+    int maxsize = ObjectList.getObject(object).getSizes().length;
 
     if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
       {
@@ -366,7 +366,7 @@ public class RubikScores
 
   public long getRecord(int object, int size, int level)
     {
-    int maxsize = RubikObjectList.getObject(object).getSizes().length;
+    int maxsize = ObjectList.getObject(object).getSizes().length;
 
     if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
       {
@@ -380,7 +380,7 @@ public class RubikScores
 
   public boolean isSubmitted(int object, int size, int level)
     {
-    int maxsize = RubikObjectList.getObject(object).getSizes().length;
+    int maxsize = ObjectList.getObject(object).getSizes().length;
 
     if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
       {
@@ -436,12 +436,12 @@ public class RubikScores
 
   boolean thereAreUnsubmittedRecords()
     {
-    RubikObjectList list;
+    ObjectList list;
     int length;
 
     for(int object=0; object<NUM_OBJECTS; object++)
       {
-      list = RubikObjectList.getObject(object);
+      list = ObjectList.getObject(object);
       length = list.getSizes().length;
 
       for(int size=0; size<length; size++)
diff --git a/src/main/java/org/distorted/scores/RubikScoresDownloader.java b/src/main/java/org/distorted/scores/RubikScoresDownloader.java
index c124d90f..235deea6 100644
--- a/src/main/java/org/distorted/scores/RubikScoresDownloader.java
+++ b/src/main/java/org/distorted/scores/RubikScoresDownloader.java
@@ -24,7 +24,7 @@ import android.content.pm.PackageManager;
 
 import androidx.fragment.app.FragmentActivity;
 
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 
 import java.io.InputStream;
 import java.net.HttpURLConnection;
@@ -33,7 +33,7 @@ import java.net.UnknownHostException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
-import static org.distorted.objects.RubikObjectList.MAX_LEVEL;
+import static org.distorted.objects.ObjectList.MAX_LEVEL;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -92,7 +92,7 @@ public class RubikScoresDownloader implements Runnable
   private static Receiver mReceiver;
   private static String mVersion;
 
-  private static int mTotal = RubikObjectList.getTotal();
+  private static int mTotal = ObjectList.getTotal();
   private static String mScores = "";
   private static String[][][] mCountry = new String[mTotal][MAX_LEVEL][MAX_PLACES];
   private static String[][][] mName    = new String[mTotal][MAX_LEVEL][MAX_PLACES];
@@ -186,7 +186,7 @@ public class RubikScoresDownloader implements Runnable
 
     if( s5>s4 && s4>s3 && s3>s2 && s2>s1 && s1>0 )
       {
-      int object = RubikObjectList.unpackObjectFromString( row.substring(0,s1) );
+      int object = ObjectList.unpackObjectFromString( row.substring(0,s1) );
 
       if( object>=0 && object<mTotal )
         {
@@ -353,7 +353,7 @@ public class RubikScoresDownloader implements Runnable
 
     String url="https://distorted.org/magic/cgi-bin/download.cgi";
     url += "?n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion+"d";
-    url += "&o="+RubikObjectList.getObjectList()+"&min=0&max="+MAX_LEVEL+"&l="+MAX_PLACES;
+    url += "&o="+ ObjectList.getObjectList()+"&min=0&max="+MAX_LEVEL+"&l="+MAX_PLACES;
 
     return url;
     }
@@ -378,7 +378,7 @@ public class RubikScoresDownloader implements Runnable
     String url1="https://distorted.org/magic/cgi-bin/submit.cgi";
     String url2 = "n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&i="+deviceID+"&e="+mVersion+"d";
     url2 += "&o="+objlist+"&l="+lvllist+"&t="+timlist+"&c="+country+"&f="+epoch;
-    url2 += "&oo="+RubikObjectList.getObjectList()+"&min=0&max="+MAX_LEVEL+"&lo="+MAX_PLACES;
+    url2 += "&oo="+ ObjectList.getObjectList()+"&min=0&max="+MAX_LEVEL+"&lo="+MAX_PLACES;
     url2 += "&h="+computeHash( url2, salt.getBytes() );
 
     return url1 + "?" + url2;
diff --git a/src/main/java/org/distorted/solvers/ImplementedSolversList.java b/src/main/java/org/distorted/solvers/ImplementedSolversList.java
index c631bd53..1473ffb5 100644
--- a/src/main/java/org/distorted/solvers/ImplementedSolversList.java
+++ b/src/main/java/org/distorted/solvers/ImplementedSolversList.java
@@ -19,18 +19,18 @@
 
 package org.distorted.solvers;
 
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public enum ImplementedSolversList
 {
-  CUBE3 ( RubikObjectList.CUBE, 3),
+  CUBE3 ( ObjectList.CUBE, 3),
   ;
 
   public static final int NUM_OBJECTS = values().length;
 
-  private final RubikObjectList mObject;
+  private final ObjectList mObject;
   private final int mObjectSize;
 
   private static final ImplementedSolversList[] objects;
@@ -48,7 +48,7 @@ public enum ImplementedSolversList
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public static RubikObjectList getObject(int ordinal)
+  public static ObjectList getObject(int ordinal)
     {
     return objects[ordinal].mObject;
     }
@@ -62,7 +62,7 @@ public enum ImplementedSolversList
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ImplementedSolversList( RubikObjectList object, int size)
+  ImplementedSolversList(ObjectList object, int size)
     {
     mObject     = object;
     mObjectSize =  size;
diff --git a/src/main/java/org/distorted/solvers/SolverMain.java b/src/main/java/org/distorted/solvers/SolverMain.java
index 486bdc4a..b6ab9433 100644
--- a/src/main/java/org/distorted/solvers/SolverMain.java
+++ b/src/main/java/org/distorted/solvers/SolverMain.java
@@ -22,7 +22,7 @@ package org.distorted.solvers;
 import android.content.res.Resources;
 
 import org.distorted.main.R;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.states.RubikState;
 import org.distorted.states.RubikStateSolver;
 
@@ -32,12 +32,12 @@ public class SolverMain implements Runnable
 {
   private String mObjectPosition;
   private Resources mRes;
-  private RubikObjectList mObject;
+  private ObjectList mObject;
   private int mSize;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public SolverMain(Resources res, RubikObjectList object, int size, String position )
+  public SolverMain(Resources res, ObjectList object, int size, String position )
     {
     mRes            = res;
     mObject         = object;
@@ -51,9 +51,9 @@ public class SolverMain implements Runnable
 // If a certain cubit is locked, return the color (index into it's FACE_COLORS array) it
 // must have. Otherwise return -1.
 
-  public static int cubitIsLocked(RubikObjectList object, int size, int cubit)
+  public static int cubitIsLocked(ObjectList object, int size, int cubit)
     {
-    if( object == RubikObjectList.CUBE && size == 3)
+    if( object == ObjectList.CUBE && size == 3)
       {
       if( cubit==21 ) return 0; // center of the right  face
       if( cubit== 4 ) return 1; // center of the left   face
@@ -121,7 +121,7 @@ public class SolverMain implements Runnable
     {
     RubikStateSolver solver = (RubikStateSolver) RubikState.SVER.getStateClass();
 
-    if( mObject == RubikObjectList.CUBE && mSize == 3)
+    if( mObject == ObjectList.CUBE && mSize == 3)
       {
       solveCube3(solver);
       }
diff --git a/src/main/java/org/distorted/states/RubikStatePattern.java b/src/main/java/org/distorted/states/RubikStatePattern.java
index 64cc5493..7f9c0e33 100644
--- a/src/main/java/org/distorted/states/RubikStatePattern.java
+++ b/src/main/java/org/distorted/states/RubikStatePattern.java
@@ -34,7 +34,7 @@ import org.distorted.dialogs.RubikDialogPattern;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.ObjectList;
 import org.distorted.patterns.RubikPattern;
 import org.distorted.patterns.RubikPatternList;
 
@@ -62,7 +62,7 @@ public class RubikStatePattern extends RubikStateAbstract
     {
     RubikStatePlay play = (RubikStatePlay)RubikState.PLAY.getStateClass();
 
-    RubikObjectList object = RubikPatternList.getObject(mPatternOrdinal);
+    ObjectList object = RubikPatternList.getObject(mPatternOrdinal);
     int size = RubikPatternList.getSize(mPatternOrdinal);
 
     if( !play.setObjectAndSize(act,object,size) )
@@ -70,7 +70,7 @@ public class RubikStatePattern extends RubikStateAbstract
       int objectPlay= play.getObject();
       int sizePlay  = play.getSize();
 
-      act.changeObject(RubikObjectList.getObject(objectPlay),sizePlay, false);
+      act.changeObject(ObjectList.getObject(objectPlay),sizePlay, false);
       }
     }
 
@@ -90,7 +90,7 @@ public class RubikStatePattern extends RubikStateAbstract
 
     if( mPatternOrdinal<0 )
       {
-      mPatternOrdinal = RubikObjectList.getSizeIndex(RubikStatePlay.DEF_OBJECT,RubikStatePlay.DEF_SIZE);
+      mPatternOrdinal = ObjectList.getSizeIndex(RubikStatePlay.DEF_OBJECT,RubikStatePlay.DEF_SIZE);
       }
 
     FragmentManager mana = act.getSupportFragmentManager();
diff --git a/src/main/java/org/distorted/states/RubikStatePlay.java b/src/main/java/org/distorted/states/RubikStatePlay.java
index e5ddd7b6..06768889 100644
--- a/src/main/java/org/distorted/states/RubikStatePlay.java
+++ b/src/main/java/org/distorted/states/RubikStatePlay.java
@@ -38,8 +38,8 @@ import org.distorted.dialogs.RubikDialogScores;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.objects.ObjectList;
 import org.distorted.scores.RubikScores;
 
 import java.util.ArrayList;
@@ -50,7 +50,7 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
   {
   private static final int DURATION_MILLIS = 750;
   private static final int LEVELS_SHOWN = 10;
-  public  static final int DEF_OBJECT= RubikObjectList.CUBE.ordinal();
+  public  static final int DEF_OBJECT= ObjectList.CUBE.ordinal();
   public  static final int DEF_SIZE  =  3;
 
   private static int[] BUTTON_LABELS = { R.string.scores, R.string.patterns, R.string.solver, R.string.about };
@@ -103,8 +103,8 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
     if( mMoves==null ) mMoves = new ArrayList<>();
     else               mMoves.clear();
 
-    mRowCount = RubikObjectList.getRowCount();
-    mColCount = RubikObjectList.getColumnCount();
+    mRowCount = ObjectList.getRowCount();
+    mColCount = ObjectList.getColumnCount();
 
     // TOP ////////////////////////////
     LinearLayout layoutTop = act.findViewById(R.id.upperBar);
@@ -219,8 +219,8 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
         View popupView = mPlayPopup.getContentView();
         popupView.setSystemUiVisibility(RubikActivity.FLAGS);
 
-        final int sizeIndex = RubikObjectList.getSizeIndex(mObject,mSize);
-        final int maxLevel = RubikObjectList.getMaxLevel(mObject, sizeIndex);
+        final int sizeIndex = ObjectList.getSizeIndex(mObject,mSize);
+        final int maxLevel = ObjectList.getMaxLevel(mObject, sizeIndex);
         final int levelsShown = Math.min(maxLevel,LEVELS_SHOWN);
 
         mPlayPopup.showAsDropDown(view, margin, margin, Gravity.RIGHT);
@@ -370,7 +370,7 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
     final View layout = layoutInflater.inflate(R.layout.popup_objects, null);
     GridLayout objectGrid = layout.findViewById(R.id.objectGrid);
 
-    int[] indices = RubikObjectList.getIndices();
+    int[] indices = ObjectList.getIndices();
 
     GridLayout.Spec[] rowSpecs = new GridLayout.Spec[mRowCount];
     GridLayout.Spec[] colSpecs = new GridLayout.Spec[mColCount];
@@ -400,9 +400,9 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
     int margin = (int)(width*RubikActivity.LARGE_MARGIN);
     mObjectSize = (int)(cubeWidth + 2*margin + 0.5f);
 
-    for(int object=0; object<RubikObjectList.NUM_OBJECTS; object++)
+    for(int object = 0; object< ObjectList.NUM_OBJECTS; object++)
       {
-      final RubikObjectList list = RubikObjectList.getObject(object);
+      final ObjectList list = ObjectList.getObject(object);
       final int[] sizes = list.getSizes();
       int[] icons = list.getIconIDs();
       int len = sizes.length;
@@ -520,7 +520,7 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
       if( numMoves>0 )
         {
         Move move = mMoves.remove(numMoves-1);
-        RubikObject object = pre.getObject();
+        TwistyObject object = pre.getObject();
 
         int axis  = move.mAxis;
         int row   = (1<<move.mRow);
@@ -549,10 +549,10 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
       case 0: RubikStatePlay play = (RubikStatePlay) RubikState.PLAY.getStateClass();
               int object = play.getObject();
               int size   = play.getSize();
-              int sizeIndex = RubikObjectList.getSizeIndex(object,size);
+              int sizeIndex = ObjectList.getSizeIndex(object,size);
 
               Bundle bundle = new Bundle();
-              bundle.putInt("tab", RubikObjectList.pack(object,sizeIndex) );
+              bundle.putInt("tab", ObjectList.pack(object,sizeIndex) );
               bundle.putBoolean("submitting", false);
 
               RubikDialogScores scores = new RubikDialogScores();
@@ -627,7 +627,7 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public boolean setObjectAndSize(RubikActivity act, RubikObjectList obj, int size)
+  public boolean setObjectAndSize(RubikActivity act, ObjectList obj, int size)
     {
     if( mObject!=obj.ordinal() || mSize != size )
       {
@@ -658,8 +658,8 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
 
   private void adjustLevels(final RubikActivity act)
     {
-    int sizeIndex = RubikObjectList.getSizeIndex(mObject,mSize);
-    int maxLevel = RubikObjectList.getMaxLevel(mObject, sizeIndex);
+    int sizeIndex = ObjectList.getSizeIndex(mObject,mSize);
+    int maxLevel = ObjectList.getMaxLevel(mObject, sizeIndex);
     String[] levels = new String[maxLevel];
 
     for(int i=0; i<maxLevel; i++)
@@ -682,7 +682,7 @@ public class RubikStatePlay extends RubikStateAbstract implements RubikPreRender
 
     mPlayLayout.removeAllViews();
 
-    int realSize= RubikObjectList.getSizeIndex(mObject,mSize);
+    int realSize= ObjectList.getSizeIndex(mObject,mSize);
     RubikScores scores = RubikScores.getInstance();
 
     for(int i=0; i<maxLevel; i++)
diff --git a/src/main/java/org/distorted/states/RubikStateSolution.java b/src/main/java/org/distorted/states/RubikStateSolution.java
index 322fbd21..d254f650 100644
--- a/src/main/java/org/distorted/states/RubikStateSolution.java
+++ b/src/main/java/org/distorted/states/RubikStateSolution.java
@@ -24,7 +24,6 @@ import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -32,7 +31,7 @@ import android.widget.TextView;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
+import org.distorted.objects.TwistyObject;
 import org.distorted.patterns.RubikPattern;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -52,7 +51,7 @@ public class RubikStateSolution extends RubikStateAbstract implements RubikPreRe
 
   void leaveState(RubikActivity act)
     {
-    RubikObject object = act.getObject();
+    TwistyObject object = act.getObject();
     object.solve();
     }
 
diff --git a/src/main/java/org/distorted/states/RubikStateSolver.java b/src/main/java/org/distorted/states/RubikStateSolver.java
index abfb7047..bff415bd 100644
--- a/src/main/java/org/distorted/states/RubikStateSolver.java
+++ b/src/main/java/org/distorted/states/RubikStateSolver.java
@@ -35,8 +35,8 @@ import org.distorted.dialogs.RubikDialogSolverError;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.objects.ObjectList;
 import org.distorted.solvers.ImplementedSolversList;
 import org.distorted.solvers.SolverMain;
 
@@ -55,7 +55,7 @@ public class RubikStateSolver extends RubikStateAbstract
   private int mNumFaces;
   private float mBitmapSize;
 
-  private RubikObjectList mCurrentObject;
+  private ObjectList mCurrentObject;
   private int mCurrentObjectSize;
 
   private WeakReference<RubikActivity> mWeakAct;
@@ -90,7 +90,7 @@ public class RubikStateSolver extends RubikStateAbstract
     RubikStatePlay play = (RubikStatePlay)RubikState.PLAY.getStateClass();
     play.setObjectAndSize(act, mCurrentObject, mCurrentObjectSize);
 
-    mFaceColors = RubikObjectList.retFaceColors(mCurrentObject);
+    mFaceColors = ObjectList.retFaceColors(mCurrentObject);
     mNumFaces   = mFaceColors!=null ? mFaceColors.length : 0;
 
     // TOP ////////////////////////////
@@ -222,7 +222,7 @@ public class RubikStateSolver extends RubikStateAbstract
         if( !mSolving )
           {
           mSolving = true;
-          RubikObject object = act.getObject();
+          TwistyObject object = act.getObject();
           String objectString = object.retObjectString();
           SolverMain solver = new SolverMain( act.getResources(), mCurrentObject, mCurrentObjectSize, objectString );
           solver.start();
diff --git a/src/main/java/org/distorted/states/RubikStateSolving.java b/src/main/java/org/distorted/states/RubikStateSolving.java
index d5cd6f28..b6339499 100644
--- a/src/main/java/org/distorted/states/RubikStateSolving.java
+++ b/src/main/java/org/distorted/states/RubikStateSolving.java
@@ -30,8 +30,8 @@ import android.widget.TextView;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.objects.ObjectList;
 import org.distorted.scores.RubikScores;
 
 import java.util.ArrayList;
@@ -229,7 +229,7 @@ public class RubikStateSolving extends RubikStateAbstract implements RubikPreRen
       if( numMoves>0 )
         {
         Move move = mMoves.remove(numMoves-1);
-        RubikObject object = pre.getObject();
+        TwistyObject object = pre.getObject();
 
         int axis  = move.mAxis;
         int row   = (1<<move.mRow);
@@ -351,7 +351,7 @@ public class RubikStateSolving extends RubikStateAbstract implements RubikPreRen
       int object  = play.getObject();
       int size    = play.getSize();
       int level   = play.getLevel();
-      int realSize= RubikObjectList.getSizeIndex(object,size);
+      int realSize= ObjectList.getSizeIndex(object,size);
 
       boolean isNew = mScores.setRecord(object, realSize, level, mElapsed);
 
