commit bdbbb4c574bc6a5457e98c7dc56f26ccc040629b
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Tue Sep 28 23:47:32 2021 +0200

    Refactoring: split the 'objects' package into two, 'objects' and 'objectlib'.
    The point: we're going to need to move the 'objectlib' stuff into its own library module, and that's because we're going to create a new app module which needs access to it.

diff --git a/build.gradle b/build.gradle
index ea31ffe0..acbc5977 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,7 +37,7 @@ dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation 'com.google.firebase:firebase-analytics:19.0.1'
     implementation 'com.google.firebase:firebase-crashlytics:18.2.1'
-    implementation 'com.google.android.play:core:1.10.1'
+    implementation 'com.google.android.play:core:1.10.2'
 
     api project(':distorted-library')
     implementation 'androidx.appcompat:appcompat:1.3.1'
diff --git a/src/main/java/org/distorted/control/RubikControl.java b/src/main/java/org/distorted/control/RubikControl.java
index 87ae6b5c..6e9b67c5 100644
--- a/src/main/java/org/distorted/control/RubikControl.java
+++ b/src/main/java/org/distorted/control/RubikControl.java
@@ -26,7 +26,7 @@ import org.distorted.library.message.EffectListener;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikSurfaceView;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 
 import java.lang.ref.WeakReference;
 
diff --git a/src/main/java/org/distorted/control/RubikControlRotate.java b/src/main/java/org/distorted/control/RubikControlRotate.java
index 9638439c..b1d3a122 100644
--- a/src/main/java/org/distorted/control/RubikControlRotate.java
+++ b/src/main/java/org/distorted/control/RubikControlRotate.java
@@ -22,7 +22,7 @@ package org.distorted.control;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 
-import org.distorted.helpers.QuatHelper;
+import org.distorted.objectlb.QuatHelper;
 import org.distorted.library.effect.MatrixEffectQuaternion;
 import org.distorted.library.effect.MatrixEffectScale;
 import org.distorted.library.main.DistortedEffects;
@@ -38,7 +38,7 @@ import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 
 import java.io.IOException;
 import java.io.InputStream;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
index 517867ef..4fb1f77f 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
@@ -37,7 +37,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.network.RubikScores;
 import org.distorted.screens.ScreenList;
 import org.distorted.screens.RubikScreenPlay;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPattern.java b/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
index 2aeacd9a..c2a35ee9 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.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.patterns.RubikPatternList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java b/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
index 3cfe697a..91accccd 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.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.patterns.RubikPattern;
 import org.distorted.patterns.RubikPatternList;
 import org.distorted.screens.ScreenList;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScores.java b/src/main/java/org/distorted/dialogs/RubikDialogScores.java
index 19969863..28390de8 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScores.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScores.java
@@ -34,14 +34,13 @@ import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.Window;
-import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
index ccb599bc..a01f4537 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
@@ -33,7 +33,7 @@ import android.widget.LinearLayout;
 import org.distorted.main.R;
 import org.distorted.network.RubikScores;
 import org.distorted.network.RubikNetwork;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.screens.RubikScreenPlay;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
index ff6f326a..b8aaa36a 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.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.network.RubikScores;
 
 import static org.distorted.network.RubikNetwork.MAX_PLACES;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
index 6b0b1e77..0c5c9536 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.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.network.RubikScores;
 import org.distorted.screens.ScreenList;
 import org.distorted.screens.RubikScreenPlay;
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java b/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java
index b0d544ab..379a57be 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java
@@ -38,7 +38,7 @@ import com.google.firebase.analytics.FirebaseAnalytics;
 import org.distorted.main.BuildConfig;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.tutorials.TutorialList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/effects/EffectController.java b/src/main/java/org/distorted/effects/EffectController.java
index 856dc46b..85639f07 100644
--- a/src/main/java/org/distorted/effects/EffectController.java
+++ b/src/main/java/org/distorted/effects/EffectController.java
@@ -21,7 +21,7 @@ package org.distorted.effects;
 
 import org.distorted.helpers.MovesFinished;
 import org.distorted.library.message.EffectListener;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/effects/objectchange/ObjectChangeEffect.java b/src/main/java/org/distorted/effects/objectchange/ObjectChangeEffect.java
index bb3dfacf..9041decf 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.effects.EffectController;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 
 import java.lang.reflect.Method;
 
diff --git a/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java b/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
index 909da589..155853fd 100644
--- a/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
+++ b/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
@@ -26,8 +26,8 @@ import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
 import org.distorted.effects.EffectController;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.TwistyObject;
 
 import java.lang.reflect.Method;
 import java.util.Random;
diff --git a/src/main/java/org/distorted/effects/solve/SolveEffect.java b/src/main/java/org/distorted/effects/solve/SolveEffect.java
index ed2f1d4e..14752d48 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.effects.EffectController;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 
 import java.lang.reflect.Method;
 
diff --git a/src/main/java/org/distorted/effects/win/WinEffect.java b/src/main/java/org/distorted/effects/win/WinEffect.java
index b65565e3..6f06e5d7 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.effects.EffectController;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 
 import java.lang.reflect.Method;
 
diff --git a/src/main/java/org/distorted/helpers/FactoryCubit.java b/src/main/java/org/distorted/helpers/FactoryCubit.java
deleted file mode 100644
index 2a1ae53a..00000000
--- a/src/main/java/org/distorted/helpers/FactoryCubit.java
+++ /dev/null
@@ -1,940 +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.helpers;
-
-import org.distorted.library.effect.MatrixEffectMove;
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.effect.MatrixEffectScale;
-import org.distorted.library.effect.VertexEffect;
-import org.distorted.library.effect.VertexEffectDeform;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshJoined;
-import org.distorted.library.mesh.MeshPolygon;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class FactoryCubit
-  {
-  private static final Static1D RADIUS = new Static1D(1);
-  private static FactoryCubit mThis;
-
-  private static final double[] mBuffer = new double[3];
-  private static final double[] mQuat1  = new double[4];
-  private static final double[] mQuat2  = new double[4];
-  private static final double[] mQuat3  = new double[4];
-  private static final double[] mQuat4  = new double[4];
-
-  private static class StickerCoords
-    {
-    double[] vertices;
-    }
-
-  private static class FaceTransform
-    {
-    int sticker;
-    double vx,vy,vz;
-    double scale;
-    double qx,qy,qz,qw;
-    boolean flip;
-    }
-
-  private static final ArrayList<FaceTransform> mNewFaceTransf = new ArrayList<>();
-  private static final ArrayList<FaceTransform> mOldFaceTransf = new ArrayList<>();
-  private static final ArrayList<StickerCoords> mStickerCoords = new ArrayList<>();
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private FactoryCubit()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static FactoryCubit getInstance()
-    {
-    if( mThis==null ) mThis = new FactoryCubit();
-
-    return mThis;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// H - height of the band in the middle
-// alpha - angle of the edge  [0,90]
-// dist - often in a polygon the distance from edge to center is not 1, but something else.
-// This is the distance.
-// K - where to begin the second, much more flat part of the band. [0,1]
-// N - number of bands. N>=3
-//
-// theory: two distinct parts to the band:
-// 1) (0,B) - steep
-// 2) (B,1) - flat
-//
-// In first part, we have y = g(x) ; in second - y = g(f(x)) where
-//
-// g(x) = sqrt( R^2 - (x-D)^2 ) - R*cos(alpha)
-// f(x) = ((D-B)/(1-B)*x + B*(1-D)/(1-B)
-// h(x) = R*(sin(alpha) - sin(x))
-// R = H/(1-cos(alpha))
-// D = H*sin(alpha)
-// B = h(K*alpha)
-//
-// The N points are taken at:
-//
-// 1) in the second part, there are K2 = (N-3)/3 such points
-// 2) in the first - K1 = (N-3) - K2
-// 3) also, the 3 points 0,B,1
-//
-// so we have the sequence A[i] of N points
-//
-// 0
-// h((i+1)*(1-K)*alpha/(K1+1)) (i=0,1,...,K1-1)
-// B
-// (1-B)*(i+1)/(K2+1) + B   (i=0,i,...,K2-1)
-// 1
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float f(float D, float B, float x)
-    {
-    return ((D-B)*x + B*(1-D))/(1-B);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float g(float R, float D, float x, float cosAlpha)
-    {
-    float d = x-D;
-    return (float)(Math.sqrt(R*R-d*d)-R*cosAlpha);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float h(float R, float sinAlpha, float x)
-    {
-    return R*(sinAlpha-(float)Math.sin(x));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean areColinear(double[][] vertices, int index1, int index2, int index3)
-    {
-    double x1 = vertices[index1][0];
-    double y1 = vertices[index1][1];
-    double z1 = vertices[index1][2];
-    double x2 = vertices[index2][0];
-    double y2 = vertices[index2][1];
-    double z2 = vertices[index2][2];
-    double x3 = vertices[index3][0];
-    double y3 = vertices[index3][1];
-    double z3 = vertices[index3][2];
-
-    double v1x = x2-x1;
-    double v1y = y2-y1;
-    double v1z = z2-z1;
-    double v2x = x3-x1;
-    double v2y = y3-y1;
-    double v2z = z3-z1;
-
-    double A = Math.sqrt( (v1x*v1x+v1y*v1y+v1z*v1z) / (v2x*v2x+v2y*v2y+v2z*v2z) );
-
-    return (v1x==A*v2x && v1y==A*v2y && v1z==A*v2z);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void computeNormalVector(double[][] vertices, int index1, int index2, int index3)
-    {
-    double x1 = vertices[index1][0];
-    double y1 = vertices[index1][1];
-    double z1 = vertices[index1][2];
-    double x2 = vertices[index2][0];
-    double y2 = vertices[index2][1];
-    double z2 = vertices[index2][2];
-    double x3 = vertices[index3][0];
-    double y3 = vertices[index3][1];
-    double z3 = vertices[index3][2];
-
-    double v1x = x2-x1;
-    double v1y = y2-y1;
-    double v1z = z2-z1;
-    double v2x = x3-x1;
-    double v2y = y3-y1;
-    double v2z = z3-z1;
-
-    mBuffer[0] = v1y*v2z - v2y*v1z;
-    mBuffer[1] = v1z*v2x - v2z*v1x;
-    mBuffer[2] = v1x*v2y - v2x*v1y;
-
-    double len = mBuffer[0]*mBuffer[0] + mBuffer[1]*mBuffer[1] + mBuffer[2]*mBuffer[2];
-    len = Math.sqrt(len);
-    mBuffer[0] /= len;
-    mBuffer[1] /= len;
-    mBuffer[2] /= len;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return quat1*quat2
-
-  private static void quatMultiply( double[] quat1, double[] quat2, double[] result )
-    {
-    double qx = quat1[0];
-    double qy = quat1[1];
-    double qz = quat1[2];
-    double qw = quat1[3];
-
-    double rx = quat2[0];
-    double ry = quat2[1];
-    double rz = quat2[2];
-    double rw = quat2[3];
-
-    result[0] = rw*qx - rz*qy + ry*qz + rx*qw;
-    result[1] = rw*qy + rz*qx + ry*qw - rx*qz;
-    result[2] = rw*qz + rz*qw - ry*qx + rx*qy;
-    result[3] = rw*qw - rz*qz - ry*qy - rx*qx;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void fitInSquare(FaceTransform info, double[][] vert3D)
-    {
-    double minX = Double.MAX_VALUE;
-    double maxX =-Double.MAX_VALUE;
-    double minY = Double.MAX_VALUE;
-    double maxY =-Double.MAX_VALUE;
-
-    for (double[] vert : vert3D)
-      {
-      double x = vert[0];
-      double y = vert[1];
-
-      if (x > maxX) maxX = x;
-      if (x < minX) minX = x;
-      if (y > maxY) maxY = y;
-      if (y < minY) minY = y;
-      }
-
-    minX = minX<0 ? -minX:minX;
-    maxX = maxX<0 ? -maxX:maxX;
-    minY = minY<0 ? -minY:minY;
-    maxY = maxY<0 ? -maxY:maxY;
-
-    double max1 = Math.max(minX,minY);
-    double max2 = Math.max(maxX,maxY);
-    double max3 = Math.max(max1,max2);
-
-    info.scale = max3/0.5;
-
-    int len = vert3D.length;
-    StickerCoords sInfo = new StickerCoords();
-    sInfo.vertices = new double[2*len];
-
-    for( int vertex=0; vertex<len; vertex++ )
-      {
-      sInfo.vertices[2*vertex  ] = vert3D[vertex][0] / info.scale;
-      sInfo.vertices[2*vertex+1] = vert3D[vertex][1] / info.scale;
-      }
-
-    mStickerCoords.add(sInfo);
-
-    info.sticker = mStickerCoords.size() -1;
-    info.flip = false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private FaceTransform constructNewTransform(final double[][] vert3D)
-    {
-    FaceTransform ft = new FaceTransform();
-
-    // compute center of gravity
-    ft.vx = 0.0f;
-    ft.vy = 0.0f;
-    ft.vz = 0.0f;
-    int len = vert3D.length;
-
-    for (double[] vert : vert3D)
-      {
-      ft.vx += vert[0];
-      ft.vy += vert[1];
-      ft.vz += vert[2];
-      }
-
-    ft.vx /= len;
-    ft.vy /= len;
-    ft.vz /= len;
-
-    // move all vertices so that their center of gravity is at (0,0,0)
-    for (int i=0; i<len; i++)
-      {
-      vert3D[i][0] -= ft.vx;
-      vert3D[i][1] -= ft.vy;
-      vert3D[i][2] -= ft.vz;
-      }
-
-    // find 3 non-colinear vertices
-    int foundIndex = -1;
-
-    for(int vertex=2; vertex<len; vertex++)
-      {
-      if( !areColinear(vert3D,0,1,vertex) )
-        {
-        foundIndex = vertex;
-        break;
-        }
-      }
-
-    // compute the normal vector
-    if( foundIndex==-1 )
-      {
-      throw new RuntimeException("all vertices colinear");
-      }
-
-    computeNormalVector(vert3D,0,1,foundIndex);
-
-    // rotate so that the normal vector becomes (0,0,1)
-    double axisX, axisY, axisZ;
-
-    if( mBuffer[0]!=0.0f || mBuffer[1]!=0.0f )
-      {
-      axisX = -mBuffer[1];
-      axisY =  mBuffer[0];
-      axisZ = 0.0f;
-
-      double axiLen = axisX*axisX + axisY*axisY;
-      axiLen = Math.sqrt(axiLen);
-      axisX /= axiLen;
-      axisY /= axiLen;
-      axisZ /= axiLen;
-      }
-    else
-      {
-      axisX = 0.0f;
-      axisY = 1.0f;
-      axisZ = 0.0f;
-      }
-
-    double cosTheta = mBuffer[2];
-    double sinTheta = Math.sqrt(1-cosTheta*cosTheta);
-    double sinHalfTheta = computeSinHalf(cosTheta);
-    double cosHalfTheta = computeCosHalf(sinTheta,cosTheta);
-
-    mQuat1[0] = axisX*sinHalfTheta;
-    mQuat1[1] = axisY*sinHalfTheta;
-    mQuat1[2] = axisZ*sinHalfTheta;
-    mQuat1[3] = cosHalfTheta;
-    mQuat2[0] =-axisX*sinHalfTheta;
-    mQuat2[1] =-axisY*sinHalfTheta;
-    mQuat2[2] =-axisZ*sinHalfTheta;
-    mQuat2[3] = cosHalfTheta;
-
-    for (double[] vert : vert3D)
-      {
-      quatMultiply(mQuat1, vert  , mQuat3);
-      quatMultiply(mQuat3, mQuat2, vert  );
-      }
-
-    // fit the whole thing in a square and remember the scale & 2D vertices
-    fitInSquare(ft, vert3D);
-
-    // remember the rotation
-    ft.qx =-mQuat1[0];
-    ft.qy =-mQuat1[1];
-    ft.qz =-mQuat1[2];
-    ft.qw = mQuat1[3];
-
-    return ft;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void rotateAllVertices(double[] result, int len, double[] vertices, double sin, double cos)
-    {
-    for(int i=0; i<len; i++)
-      {
-      result[2*i  ] = vertices[2*i  ]*cos - vertices[2*i+1]*sin;
-      result[2*i+1] = vertices[2*i  ]*sin + vertices[2*i+1]*cos;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private double computeScale(double[] v1, double[] v2, int v1i, int v2i)
-    {
-    double v1x = v1[2*v1i];
-    double v1y = v1[2*v1i+1];
-    double v2x = v2[2*v2i];
-    double v2y = v2[2*v2i+1];
-
-    double lenSq1 = v1x*v1x + v1y*v1y;
-    double lenSq2 = v2x*v2x + v2y*v2y;
-
-    return Math.sqrt(lenSq2/lenSq1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// valid for 0<angle<2*PI
-
-  private double computeSinHalf(double cos)
-    {
-    return Math.sqrt((1-cos)/2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// valid for 0<angle<2*PI
-
-  private double computeCosHalf(double sin, double cos)
-    {
-    double cosHalf = Math.sqrt((1+cos)/2);
-    return sin<0 ? -cosHalf : cosHalf;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int computeRotatedIndex(int oldVertex, int len, int rotatedVertex, boolean inverted)
-    {
-    int v = (rotatedVertex + (inverted? -oldVertex : oldVertex));
-    if( v>=len ) v-=len;
-    if( v< 0   ) v+=len;
-
-    return v;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean isScaledVersionOf(double[] newVert, double[] oldVert, int len, int vertex, boolean inverted)
-    {
-    int newZeroIndex = computeRotatedIndex(0,len,vertex,inverted);
-    double EPSILON = 0.001;
-    double scale = computeScale(newVert,oldVert,newZeroIndex,0);
-
-    for(int i=1; i<len; i++)
-      {
-      int index = computeRotatedIndex(i,len,vertex,inverted);
-
-      double horz = oldVert[2*i  ] - scale*newVert[2*index  ];
-      double vert = oldVert[2*i+1] - scale*newVert[2*index+1];
-
-      if( horz>EPSILON || horz<-EPSILON || vert>EPSILON || vert<-EPSILON ) return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void mirrorAllVertices(double[] output, int len, double[] input)
-    {
-    for(int vertex=0; vertex<len; vertex++)
-      {
-      output[2*vertex  ] = input[2*vertex  ];
-      output[2*vertex+1] =-input[2*vertex+1];
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void correctInfo(FaceTransform info, double scale, double sin, double cos, int oldSticker, boolean flip)
-    {
-    mStickerCoords.remove(info.sticker);
-
-    info.flip    = flip;
-    info.sticker = oldSticker;
-    info.scale  *= scale;
-
-    mQuat1[0] = info.qx;
-    mQuat1[1] = info.qy;
-    mQuat1[2] = info.qz;
-    mQuat1[3] = info.qw;
-
-    double sinHalf = computeSinHalf(cos);
-    double cosHalf = computeCosHalf(sin,cos);
-
-    if( flip )
-      {
-      mQuat3[0] = 0.0f;
-      mQuat3[1] = 0.0f;
-      mQuat3[2] = sinHalf;
-      mQuat3[3] = cosHalf;
-
-      mQuat4[0] = 1.0;
-      mQuat4[1] = 0.0;
-      mQuat4[2] = 0.0;
-      mQuat4[3] = 0.0;
-
-      quatMultiply( mQuat3, mQuat4, mQuat2 );
-      }
-    else
-      {
-      mQuat2[0] = 0.0f;
-      mQuat2[1] = 0.0f;
-      mQuat2[2] = sinHalf;
-      mQuat2[3] = cosHalf;
-      }
-
-    quatMultiply( mQuat1, mQuat2, mQuat3 );
-
-    info.qx = mQuat3[0];
-    info.qy = mQuat3[1];
-    info.qz = mQuat3[2];
-    info.qw = mQuat3[3];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void printVert(double[] buffer)
-    {
-    int len = buffer.length/2;
-    String str = "";
-
-    for(int i=0; i<len; i++)
-      {
-      str += (" ("+buffer[2*i]+" , "+buffer[2*i+1]+" ) ");
-      }
-
-    android.util.Log.d("D", str);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean foundVertex(FaceTransform info, double[] buffer, int len, double[] newVert,
-                              double[] oldVert, double lenFirstOld, int oldSticker, boolean inverted)
-    {
-    for(int vertex=0; vertex<len; vertex++)
-      {
-      double newX = newVert[2*vertex  ];
-      double newY = newVert[2*vertex+1];
-      double lenIthNew = Math.sqrt(newX*newX + newY*newY);
-      double cos = QuatHelper.computeCos( oldVert[0], oldVert[1], newX, newY, lenIthNew, lenFirstOld);
-      double sin = QuatHelper.computeSin( oldVert[0], oldVert[1], newX, newY, lenIthNew, lenFirstOld);
-
-      rotateAllVertices(buffer,len,newVert,sin,cos);
-
-      if( isScaledVersionOf(buffer,oldVert,len,vertex,inverted) )
-        {
-        int newZeroIndex = computeRotatedIndex(0,len,vertex,inverted);
-        double scale = computeScale(oldVert,newVert,0,newZeroIndex);
-        correctInfo(info,scale,sin,cos,oldSticker,inverted);
-        return true;
-        }
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean successfullyCollapsedStickers(final FaceTransform newInfo, final FaceTransform oldInfo)
-    {
-    StickerCoords sNewInfo = mStickerCoords.get(newInfo.sticker);
-    StickerCoords sOldInfo = mStickerCoords.get(oldInfo.sticker);
-    double[] newVert = sNewInfo.vertices;
-    double[] oldVert = sOldInfo.vertices;
-    int oldLen = oldVert.length;
-    int newLen = newVert.length;
-
-    if( oldLen == newLen )
-      {
-      int oldSticker = oldInfo.sticker;
-      double[] buffer1 = new double[oldLen];
-      double lenFirstOld = Math.sqrt(oldVert[0]*oldVert[0] + oldVert[1]*oldVert[1]);
-      if( foundVertex(newInfo, buffer1, oldLen/2, newVert, oldVert, lenFirstOld, oldSticker, false) ) return true;
-      double[] buffer2 = new double[oldLen];
-      mirrorAllVertices(buffer2, newLen/2, newVert);
-      if( foundVertex(newInfo, buffer1, oldLen/2, buffer2, oldVert, lenFirstOld, oldSticker, true ) ) return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private double[][] constructVert(double[][] vertices, int[] index)
-    {
-    int len = index.length;
-    double[][] ret = new double[len][4];
-
-    for(int i=0; i<len; i++)
-      {
-      ret[i][0] = vertices[index[i]][0];
-      ret[i][1] = vertices[index[i]][1];
-      ret[i][2] = vertices[index[i]][2];
-      ret[i][3] = 1.0f;
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void prepareAndRoundCorners(MeshBase mesh, double[][] vertices,
-                                      float[][] corners, int[] cornerIndexes,
-                                      float[][] centers, int[] centerIndexes )
-    {
-    int lenV = vertices.length;
-    Static3D[] staticVert = new Static3D[1];
-    Static3D center = new Static3D(0,0,0);
-
-    for(int v=0; v<lenV; v++)
-      {
-      staticVert[0] = new Static3D( (float)vertices[v][0], (float)vertices[v][1], (float)vertices[v][2]);
-
-      int cent = centerIndexes[v];
-
-      if( cent>=0 )
-        {
-        center.set( centers[cent][0], centers[cent][1], centers[cent][2]);
-
-        int corn = cornerIndexes[v];
-
-        if( corn>=0 )
-          {
-          float strength = corners[corn][0];
-          float radius   = corners[corn][1];
-          roundCorners(mesh, center, staticVert, strength, radius);
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void correctComponents(MeshBase mesh, int numComponents)
-    {
-    int numTexToBeAdded = numComponents-mesh.getNumTexComponents();
-
-    mesh.mergeEffComponents();
-
-    for(int i=0; i<numTexToBeAdded; i++ ) mesh.addEmptyTexComponent();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void printTransform(FaceTransform f)
-    {
-    android.util.Log.e("D", "q=("+f.qx+", "+f.qy+", "+f.qz+", "+f.qw+") v=("
-                       +f.vx+", "+f.vy+", "+f.vz+") scale="+f.scale+" sticker="+f.sticker);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC
-
-  public float[] computeBands(float H, int alpha, float dist, float K, int N)
-    {
-    float[] bands = new float[2*N];
-
-    bands[0] = 1.0f;
-    bands[1] = 0.0f;
-
-    float beta = (float)Math.atan(dist*Math.tan(Math.PI*alpha/180));
-    float sinBeta = (float)Math.sin(beta);
-    float cosBeta = (float)Math.cos(beta);
-    float R = cosBeta<1.0f ? H/(1.0f-cosBeta) : 0.0f;
-    float D = R*sinBeta;
-    float B = h(R,sinBeta,K*beta);
-
-    if( D>1.0f )
-      {
-      for(int i=1; i<N; i++)
-        {
-        bands[2*i  ] = (float)(N-1-i)/(N-1);
-        bands[2*i+1] = H*(1-bands[2*i]);
-        }
-      }
-    else
-      {
-      int K2 = (int)((N-3)*K);
-      int K1 = (N-3)-K2;
-
-      for(int i=0; i<=K1; i++)
-        {
-        float angle = K*beta + (1-K)*beta*(K1-i)/(K1+1);
-        float x = h(R,sinBeta,angle);
-        bands[2*i+2] = 1.0f - x;
-        bands[2*i+3] = g(R,D,x,cosBeta);
-        }
-
-      for(int i=0; i<=K2; i++)
-        {
-        float x = (1-B)*(i+1)/(K2+1) + B;
-        bands[2*K1+2 + 2*i+2] = 1.0f - x;
-        bands[2*K1+2 + 2*i+3] = g(R,D,f(D,B,x),cosBeta);
-        }
-      }
-
-    bands[2*N-2] = 0.0f;
-    bands[2*N-1] =    H;
-
-    return bands;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void roundCorners(MeshBase mesh, Static3D center, Static3D[] vertices, float strength, float regionRadius)
-    {
-    Static4D reg= new Static4D(0,0,0,regionRadius);
-
-    float centX = center.get0();
-    float centY = center.get1();
-    float centZ = center.get2();
-
-    for (Static3D vertex : vertices)
-      {
-      float x = strength*(centX - vertex.get0());
-      float y = strength*(centY - vertex.get1());
-      float z = strength*(centZ - vertex.get2());
-
-      VertexEffect effect = new VertexEffectDeform(new Static3D(x,y,z), RADIUS, vertex, reg);
-      mesh.apply(effect);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void printStickerCoords()
-    {
-    int stickers = mStickerCoords.size();
-
-    android.util.Log.d("D", "---- STICKER COORDS ----");
-
-    for(int s=0; s<stickers; s++)
-      {
-      String ver = "{ ";
-      StickerCoords info = mStickerCoords.get(s);
-      int len = info.vertices.length/2;
-
-      for(int i =0; i<len; i++)
-        {
-        if( i!=0 ) ver += ", ";
-        ver += ( (float)info.vertices[2*i]+"f, "+(float)info.vertices[2*i+1]+"f");
-        }
-
-      ver += " }";
-      android.util.Log.d("D", ver);
-      }
-
-    android.util.Log.d("D", "---- END STICKER COORDS ----");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void printFaceTransform()
-    {
-    android.util.Log.d("D", "---- OLD FACE TRANSFORM ---");
-
-    int oldfaces = mOldFaceTransf.size();
-
-    for(int f=0; f<oldfaces; f++)
-      {
-      printTransform(mOldFaceTransf.get(f));
-      }
-
-    android.util.Log.d("D", "---- NEW FACE TRANSFORM ---");
-
-    int newfaces = mNewFaceTransf.size();
-
-    for(int f=0; f<newfaces; f++)
-      {
-      printTransform(mNewFaceTransf.get(f));
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void clear()
-    {
-    mStickerCoords.clear();
-    mNewFaceTransf.clear();
-    mOldFaceTransf.clear();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void createNewFaceTransform( final double[][] vertices, final int[][] indexes)
-    {
-    FaceTransform ft;
-    int numNew = mNewFaceTransf.size();
-
-    for(int i=0; i<numNew; i++)
-      {
-      ft = mNewFaceTransf.remove(0);
-      mOldFaceTransf.add(ft);
-      }
-
-    int numFaces = indexes.length;
-    int numOld = mOldFaceTransf.size();
-
-    for (int face=0; face<numFaces; face++)
-      {
-      boolean collapsed = false;
-
-      double[][] vert = constructVert(vertices, indexes[face]);
-      FaceTransform newT = constructNewTransform(vert);
-
-      for (int old=0; !collapsed && old<numOld; old++)
-        {
-        ft = mOldFaceTransf.get(old);
-        if (successfullyCollapsedStickers(newT, ft)) collapsed = true;
-        }
-
-      for (int pre=0; !collapsed && pre<face; pre++)
-        {
-        ft = mNewFaceTransf.get(pre);
-        if (successfullyCollapsedStickers(newT, ft)) collapsed = true;
-        }
-
-      mNewFaceTransf.add(newT);
-      }
-    }
-
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void createNewFaceTransform(final ObjectShape shape)
-    {
-    double[][] vertices = shape.getVertices();
-    int[][] indices = shape.getVertIndices();
-    createNewFaceTransform(vertices,indices);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void computeConvexityCenter(double[] out, float[] in, FaceTransform ft)
-    {
-    if( in==null )
-      {
-      out[0] = out[1] = 0.0f;
-      }
-    else
-      {
-      out[0] = in[0] - ft.vx;
-      out[1] = in[1] - ft.vy;
-      out[2] = in[2] - ft.vz;
-      out[3] = 1.0f;
-
-      mQuat1[0] =-ft.qx;
-      mQuat1[1] =-ft.qy;
-      mQuat1[2] =-ft.qz;
-      mQuat1[3] = ft.qw;
-
-      mQuat2[0] = -mQuat1[0];
-      mQuat2[1] = -mQuat1[1];
-      mQuat2[2] = -mQuat1[2];
-      mQuat2[3] = +mQuat1[3];
-
-      quatMultiply(mQuat1, out  , mQuat3);
-      quatMultiply(mQuat3, mQuat2, out  );
-
-      out[0] /= ft.scale;
-      out[1] /= ft.scale;
-      out[2] /= ft.scale;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public MeshBase createRoundedSolid(final ObjectShape shape)
-    {
-    double[][] vertices     = shape.getVertices();
-    int[][] vertIndexes     = shape.getVertIndices();
-    float[][] bands         = shape.getBands();
-    int[]   bandIndexes     = shape.getBandIndices();
-    float[][] corners       = shape.getCorners();
-    int[]   cornerIndexes   = shape.getCornerIndices();
-    float[][] centers       = shape.getCenters();
-    int[]   centerIndexes   = shape.getCenterIndices();
-    int numComponents       = shape.getNumComponents();
-    float[] convexityCenter = shape.getConvexityCenter();
-
-    return createRoundedSolid(vertices,vertIndexes,bands,bandIndexes,corners,cornerIndexes,
-                              centers,centerIndexes,numComponents,convexityCenter);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public MeshBase createRoundedSolid(final double[][] vertices, final int[][] vertIndexes,
-                                     final float[][] bands    , final int[]   bandIndexes,
-                                     final float[][] corners  , final int[]   cornerIndexes,
-                                     final float[][] centers  , final int[]   centerIndexes,
-                                     final int numComponents  , final float[] convexityCenter )
-    {
-    int numFaces = vertIndexes.length;
-    float[] band, bandsComputed;
-    MeshBase[] meshes = new MeshBase[numFaces];
-    FaceTransform fInfo;
-    StickerCoords sInfo;
-    double[] convexXY = new double[4];
-
-    for(int face=0; face<numFaces; face++)
-      {
-      fInfo = mNewFaceTransf.get(face);
-      sInfo = mStickerCoords.get(fInfo.sticker);
-
-      double[] verts = sInfo.vertices;
-      int lenVerts = verts.length;
-      float[] vertsFloat = new float[lenVerts];
-      for(int i=0; i<lenVerts; i++) vertsFloat[i] = (float)verts[i];
-
-      computeConvexityCenter(convexXY,convexityCenter,fInfo);
-
-      band = bands[bandIndexes[face]];
-      bandsComputed = computeBands( band[0], (int)band[1], band[2], band[3], (int)band[4]);
-      meshes[face] = new MeshPolygon(vertsFloat,bandsComputed,(int)band[5],(int)band[6], (float)convexXY[0], (float)convexXY[1]);
-      meshes[face].setEffectAssociation(0,(1<<face),0);
-      }
-
-    MeshBase mesh = new MeshJoined(meshes);
-    Static3D center = new Static3D(0,0,0);
-
-    for(int face=0; face<numFaces; face++)
-      {
-      int assoc = (1<<face);
-      fInfo = mNewFaceTransf.get(face);
-
-      float vx = (float)fInfo.vx;
-      float vy = (float)fInfo.vy;
-      float vz = (float)fInfo.vz;
-      float sc = (float)fInfo.scale;
-      float qx = (float)fInfo.qx;
-      float qy = (float)fInfo.qy;
-      float qz = (float)fInfo.qz;
-      float qw = (float)fInfo.qw;
-
-      Static3D scale = new Static3D(sc,sc, fInfo.flip ? -sc : sc);
-      Static3D move3D= new Static3D(vx,vy,vz);
-      Static4D quat  = new Static4D(qx,qy,qz,qw);
-
-      mesh.apply(new MatrixEffectScale(scale)           ,assoc,-1);
-      mesh.apply(new MatrixEffectQuaternion(quat,center),assoc,-1);
-      mesh.apply(new MatrixEffectMove(move3D)           ,assoc,-1);
-      }
-
-    prepareAndRoundCorners(mesh, vertices, corners, cornerIndexes, centers, centerIndexes);
-
-    correctComponents(mesh,numComponents);
-
-    return mesh;
-    }
-  }
diff --git a/src/main/java/org/distorted/helpers/FactorySticker.java b/src/main/java/org/distorted/helpers/FactorySticker.java
deleted file mode 100644
index f3bee856..00000000
--- a/src/main/java/org/distorted/helpers/FactorySticker.java
+++ /dev/null
@@ -1,315 +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.helpers;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import static org.distorted.objects.TwistyObject.TEXTURE_HEIGHT;
-import static org.distorted.objects.TwistyObject.COLOR_BLACK;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class FactorySticker
-  {
-  private static FactorySticker mThis;
-  private float mOX, mOY, mR;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private FactorySticker()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static FactorySticker getInstance()
-    {
-    if( mThis==null ) mThis = new FactorySticker();
-
-    return mThis;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float computeAngle(float dx, float dy)
-    {
-    float PI = (float)Math.PI;
-    double angle = Math.atan2(dy,dx);
-    float ret = (float)(3*PI/2-angle);
-
-    if( ret>2*PI ) ret-= 2*PI;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float getAngle(float[] angles, int index)
-    {
-    return angles==null ? 0 : angles[index];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void computeCircleCoords(float lX,float lY, float rX, float rY, float alpha)
-    {
-    float ctg= 1.0f/((float)Math.tan(alpha));
-    mOX = 0.5f*(lX+rX) + ctg*0.5f*(lY-rY);
-    mOY = 0.5f*(lY+rY) - ctg*0.5f*(lX-rX);
-    float dx = mOX-lX;
-    float dy = mOY-lY;
-    mR = (float)Math.sqrt(dx*dx+dy*dy);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// circle1: center (x1,y1) radius r1; circle2: center (x2,y2) radius r2.
-// Guaranteed to intersect in two points. Find the intersection. Which one? the one that's closer
-// to (nearx,neary).
-
-  private void findCircleIntersection(float x1,float y1, float r1, float x2, float y2, float r2, float nearx, float neary )
-    {
-    float dx = x2-x1;
-    float dy = y2-y1;
-    float d = (float)Math.sqrt(dx*dx+dy*dy);
-
-    if( d>0 )
-      {
-      float Dx = dx/d;
-      float Dy = dy/d;
-      float cos = (r1*r1+d*d-r2*r2)/(2*r1*d);
-      float sin = (float)Math.sqrt(1-cos*cos);
-
-      float ox1 = x1 + r1*cos*Dx + r1*sin*Dy;
-      float oy1 = y1 + r1*cos*Dy - r1*sin*Dx;
-      float ox2 = x1 + r1*cos*Dx - r1*sin*Dy;
-      float oy2 = y1 + r1*cos*Dy + r1*sin*Dx;
-
-      dx = nearx-ox1;
-      dy = neary-oy1;
-      float d1 = dx*dx+dy*dy;
-      dx = nearx-ox2;
-      dy = neary-oy2;
-      float d2 = dx*dx+dy*dy;
-
-      if( d1<d2 )
-        {
-        mOX = ox1;
-        mOY = oy1;
-        }
-      else
-        {
-        mOX = ox2;
-        mOY = oy2;
-        }
-      }
-    else
-      {
-      mOX = x1;
-      mOY = y1;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void drawCurrCurveV(Canvas canvas, Paint paint, int left, int top, float r, float stroke, float pX, float pY, float cX, float cY, float nX, float nY, float pA, float cA)
-    {
-    pX = (0.5f+pX)*TEXTURE_HEIGHT;
-    pY = (0.5f-pY)*TEXTURE_HEIGHT;
-    cX = (0.5f+cX)*TEXTURE_HEIGHT;
-    cY = (0.5f-cY)*TEXTURE_HEIGHT;
-    nX = (0.5f+nX)*TEXTURE_HEIGHT;
-    nY = (0.5f-nY)*TEXTURE_HEIGHT;
-
-    computeCircleCoords(pX,pY,cX,cY,pA);
-    float o1x = mOX;
-    float o1y = mOY;
-    float r1  = mR;
-    computeCircleCoords(cX,cY,nX,nY,cA);
-    float o2x = mOX;
-    float o2y = mOY;
-    float r2  = mR;
-
-    float dx = o1x-pX;
-    float dy = o1y-pY;
-    float startA = computeAngle(dy,dx);
-    float sweepA = 2*pA;
-
-    startA *= 180/(Math.PI);
-    sweepA *= 180/(Math.PI);
-
-    canvas.drawArc( left+o1x-r1, top+o1y-r1, left+o1x+r1, top+o1y+r1, startA, sweepA, false, paint);
-
-    float r3 = r*TEXTURE_HEIGHT + stroke/2;
-    float R1 = r1 + (pA < 0 ? r3:-r3);
-    float R2 = r2 + (cA < 0 ? r3:-r3);
-    findCircleIntersection(o1x,o1y,R1,o2x,o2y,R2,cX,cY);
-    float o3x = mOX;
-    float o3y = mOY;
-
-    dx = pA<0 ? o3x-o1x : o1x-o3x;
-    dy = pA<0 ? o3y-o1y : o1y-o3y;
-    startA = computeAngle(dy,dx);
-    dx = cA<0 ? o3x-o2x : o2x-o3x;
-    dy = cA<0 ? o3y-o2y : o2y-o3y;
-    float endA = computeAngle(dy,dx);
-
-    sweepA = endA-startA;
-    if( sweepA<0 ) sweepA += 2*Math.PI;
-
-    startA *= 180/(Math.PI);
-    sweepA *= 180/(Math.PI);
-
-    canvas.drawArc( left+o3x-r3, top+o3y-r3, left+o3x+r3, top+o3y+r3, startA, sweepA, false, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void drawCurrVertex(Canvas canvas, Paint paint, int left, int top, float r, float stroke, float pX, float pY, float cX, float cY, float nX, float nY)
-    {
-    pX = (0.5f+pX)*TEXTURE_HEIGHT;
-    pY = (0.5f-pY)*TEXTURE_HEIGHT;
-    cX = (0.5f+cX)*TEXTURE_HEIGHT;
-    cY = (0.5f-cY)*TEXTURE_HEIGHT;
-    nX = (0.5f+nX)*TEXTURE_HEIGHT;
-    nY = (0.5f-nY)*TEXTURE_HEIGHT;
-
-    canvas.drawLine(left+pX,top+pY,left+cX,top+cY,paint);
-
-    float aX = pX-cX;
-    float aY = pY-cY;
-    float bX = cX-nX;
-    float bY = cY-nY;
-
-    float aLen = (float)Math.sqrt(aX*aX+aY*aY);
-    float bLen = (float)Math.sqrt(bX*bX+bY*bY);
-
-    aX /= aLen;
-    aY /= aLen;
-    bX /= bLen;
-    bY /= bLen;
-
-    float sX = (aX-bX)/2;
-    float sY = (aY-bY)/2;
-    float sLen = (float)Math.sqrt(sX*sX+sY*sY);
-    sX /= sLen;
-    sY /= sLen;
-
-    float startAngle = computeAngle(bX,-bY);
-    float endAngle   = computeAngle(aX,-aY);
-    float sweepAngle = endAngle-startAngle;
-    if( sweepAngle<0 ) sweepAngle += 2*Math.PI;
-
-    float R = r*TEXTURE_HEIGHT+stroke/2;
-    float C = (float)Math.cos(sweepAngle/2);
-    float A = R/C;
-
-    left += (cX+A*sX);
-    top  += (cY+A*sY);
-
-    if( C< (2*R-stroke)/(2*R+stroke) )
-      {
-      float alpha = startAngle + sweepAngle/2;
-      float B  = (R-stroke/2)/C;
-      float sx = (float)Math.cos(alpha);
-      float sy = (float)Math.sin(alpha);
-
-      float startX = left + R*sx;
-      float startY = top  + R*sy;
-      float stopX  = left + B*sx;
-      float stopY  = top  + B*sy;
-
-      canvas.drawLine(startX,startY,stopX,stopY,paint);
-      }
-
-    startAngle *= 180/(Math.PI);
-    sweepAngle *= 180/(Math.PI);
-
-    canvas.drawArc( left-R, top-R, left+R, top+R, startAngle, sweepAngle, false, paint);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC
-
-  public void drawRoundedPolygon(Canvas canvas, Paint paint, int left, int top, int color, ObjectSticker sticker)
-    {
-    float stroke = sticker.getStroke();
-    float[] vertices = sticker.getCoords();
-    float[] angles = sticker.getCurvature();
-    float[] radii = sticker.getRadii();
-
-    stroke *= TEXTURE_HEIGHT;
-
-    paint.setAntiAlias(true);
-    paint.setStrokeWidth(stroke);
-    paint.setColor(color);
-    paint.setStyle(Paint.Style.FILL);
-
-    canvas.drawRect(left,top,left+TEXTURE_HEIGHT,top+TEXTURE_HEIGHT,paint);
-
-    paint.setColor(COLOR_BLACK);
-    paint.setStyle(Paint.Style.STROKE);
-
-    int length = vertices.length;
-    int numVertices = length/2;
-
-    float prevX = vertices[length-2];
-    float prevY = vertices[length-1];
-    float currX = vertices[0];
-    float currY = vertices[1];
-    float nextX = vertices[2];
-    float nextY = vertices[3];
-
-    float prevA = getAngle(angles,numVertices-1);
-    float currA = getAngle(angles,0);
-
-    for(int vert=0; vert<numVertices; vert++)
-      {
-      if( prevA==0 )
-        {
-        drawCurrVertex(canvas, paint, left, top, radii[vert], stroke, prevX,prevY,currX,currY,nextX,nextY);
-        }
-      else
-        {
-        drawCurrCurveV(canvas, paint, left, top, radii[vert], stroke, prevX,prevY,currX,currY,nextX,nextY,prevA,currA);
-        }
-
-      prevX = currX;
-      prevY = currY;
-      currX = nextX;
-      currY = nextY;
-
-      prevA = currA;
-      currA = getAngle(angles, vert==numVertices-1 ? 0 : vert+1);
-
-      if( 2*(vert+2)+1 < length )
-        {
-        nextX = vertices[2*(vert+2)  ];
-        nextY = vertices[2*(vert+2)+1];
-        }
-      else
-        {
-        nextX = vertices[0];
-        nextY = vertices[1];
-        }
-      }
-    }
-  }
diff --git a/src/main/java/org/distorted/helpers/ObjectShape.java b/src/main/java/org/distorted/helpers/ObjectShape.java
deleted file mode 100644
index dc0ccf2d..00000000
--- a/src/main/java/org/distorted/helpers/ObjectShape.java
+++ /dev/null
@@ -1,124 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ObjectShape
-  {
-  private final double[][] mVertices;
-  private final int[][] mVertIndices;
-  private final float[][] mBands;
-  private final int[] mBandIndices;
-  private final float[][] mCorners;
-  private final int[] mCornerIndices;
-  private final float[][] mCenters;
-  private final int[] mCenterIndices;
-  private final int mNumComponents;
-  private final float[] mConvexityCenter;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ObjectShape(double[][] vertices, int[][] vertIndices, float[][] bands, int[] bandIndices,
-                     float[][] corners, int[] cornIndices, float[][] centers, int[] centIndices,
-                     int numComponents, float[] convexityCenter)
-    {
-    mVertices        = vertices;
-    mVertIndices     = vertIndices;
-    mBands           = bands;
-    mBandIndices     = bandIndices;
-    mCorners         = corners;
-    mCornerIndices   = cornIndices;
-    mCenters         = centers;
-    mCenterIndices   = centIndices;
-    mNumComponents   = numComponents;
-    mConvexityCenter = convexityCenter;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public double[][] getVertices()
-    {
-    return mVertices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[][] getVertIndices()
-    {
-    return mVertIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[][] getBands()
-    {
-    return mBands;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getBandIndices()
-    {
-    return mBandIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[][] getCorners()
-    {
-    return mCorners;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getCornerIndices()
-    {
-    return mCornerIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[][] getCenters()
-    {
-    return mCenters;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getCenterIndices()
-    {
-    return mCenterIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumComponents()
-    {
-    return mNumComponents;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getConvexityCenter()
-    {
-    return mConvexityCenter;
-    }
-  }
diff --git a/src/main/java/org/distorted/helpers/ObjectSticker.java b/src/main/java/org/distorted/helpers/ObjectSticker.java
deleted file mode 100644
index 8ca437dc..00000000
--- a/src/main/java/org/distorted/helpers/ObjectSticker.java
+++ /dev/null
@@ -1,68 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ObjectSticker
-  {
-  private final float[] mCoords;
-  private final float[] mCurvature;
-  private final float[] mRadii;
-  private final float mStroke;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ObjectSticker(float[] coords, float[] curvature, float[] radii, float stroke)
-    {
-    mCoords    = coords;
-    mCurvature = curvature;
-    mRadii     = radii;
-    mStroke    = stroke;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getCoords()
-    {
-    return mCoords;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getCurvature()
-    {
-    return mCurvature;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getRadii()
-    {
-    return mRadii;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float getStroke()
-    {
-    return mStroke;
-    }
-  }
diff --git a/src/main/java/org/distorted/helpers/QuatHelper.java b/src/main/java/org/distorted/helpers/QuatHelper.java
deleted file mode 100644
index 45a76f84..00000000
--- a/src/main/java/org/distorted/helpers/QuatHelper.java
+++ /dev/null
@@ -1,163 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class QuatHelper
-  {
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return quat1*quat2
-
-  public static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
-    {
-    float qx = quat1.get0();
-    float qy = quat1.get1();
-    float qz = quat1.get2();
-    float qw = quat1.get3();
-
-    float rx = quat2.get0();
-    float ry = quat2.get1();
-    float rz = quat2.get2();
-    float rw = quat2.get3();
-
-    float tx = rw*qx - rz*qy + ry*qz + rx*qw;
-    float ty = rw*qy + rz*qx + ry*qw - rx*qz;
-    float tz = rw*qz + rz*qw - ry*qx + rx*qy;
-    float tw = rw*qw - rz*qz - ry*qy - rx*qx;
-
-    return new Static4D(tx,ty,tz,tw);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
-
-  public static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
-    {
-    float qx = quat.get0();
-    float qy = quat.get1();
-    float qz = quat.get2();
-    float qw = quat.get3();
-
-    Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-    Static4D tmp = quatMultiply(quat,vector);
-
-    return quatMultiply(tmp,quatInverted);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
-
-  public static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
-    {
-    float qx = quat.get0();
-    float qy = quat.get1();
-    float qz = quat.get2();
-    float qw = quat.get3();
-
-    Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-    Static4D tmp = quatMultiply(quatInverted,vector);
-
-    return quatMultiply(tmp,quat);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static Static4D quatFromDrag(float dragX, float dragY)
-    {
-    float axisX = dragY;  // inverted X and Y - rotation axis is perpendicular to (dragX,dragY)
-    float axisY = dragX;  // Why not (-dragY, dragX) ? because Y axis is also inverted!
-    float axisZ = 0;
-    float axisL = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ);
-
-    if( axisL>0 )
-      {
-      axisX /= axisL;
-      axisY /= axisL;
-      axisZ /= axisL;
-
-      float ratio = axisL;
-      ratio = ratio - (int)ratio;     // the cos() is only valid in (0,Pi)
-
-      float cosA = (float)Math.cos(Math.PI*ratio);
-      float sinA = (float)Math.sqrt(1-cosA*cosA);
-
-      return new Static4D(axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
-      }
-
-    return new Static4D(0f, 0f, 0f, 1f);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static double computeCos(double oldX, double oldY, double newX, double newY, double len1, double len2)
-    {
-    double ret= (oldX*newX+oldY*newY) / (len1*len2);
-    if( ret<-1.0 ) return -1.0;
-    if( ret> 1.0 ) return  1.0;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// sin of (signed!) angle between vectors 'old' and 'new', counterclockwise!
-
-  public static double computeSin(double oldX, double oldY, double newX, double newY, double len1, double len2)
-    {
-    double ret= (newX*oldY-oldX*newY) / (len1*len2);
-    if( ret<-1.0 ) return -1.0;
-    if( ret> 1.0 ) return  1.0;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return quat Q that turns 3D vector A=(ax,ay,az) to another 3D vector B=(bx,by,bz)
-// take care of double-cover by ensuring that always Q.get3() >=0
-
-  public static Static4D retRotationQuat(float ax, float ay, float az, float bx, float by, float bz)
-    {
-    float nx = ay*bz - az*by;
-    float ny = az*bx - ax*bz;
-    float nz = ax*by - ay*bx;
-
-    float sin = (float)Math.sqrt(nx*nx + ny*ny + nz*nz);
-    float cos = ax*bx + ay*by + az*bz;
-
-    if( sin!=0 )
-      {
-      nx /= sin;
-      ny /= sin;
-      nz /= sin;
-      }
-
-    // Why sin<=0 and cos>=0 ?
-    // 0<angle<180 -> 0<halfAngle<90 -> both sin and cos are positive.
-    // But1: quats work counterclockwise -> negate cos.
-    // But2: double-cover, we prefer to have the cos positive (so that unit=(0,0,0,1))
-    // so negate again both cos and sin.
-    float sinHalf =-(float)Math.sqrt((1-cos)/2);
-    float cosHalf = (float)Math.sqrt((1+cos)/2);
-
-    return new Static4D(nx*sinHalf,ny*sinHalf,nz*sinHalf,cosHalf);
-    }
-  }
diff --git a/src/main/java/org/distorted/helpers/ScrambleState.java b/src/main/java/org/distorted/helpers/ScrambleState.java
deleted file mode 100644
index 6a9da810..00000000
--- a/src/main/java/org/distorted/helpers/ScrambleState.java
+++ /dev/null
@@ -1,141 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-import java.util.Random;
-
-public class ScrambleState
-{
-  private final int mTotal, mNumAxis;
-  private final int[] mNum;
-  private final int[] mInfo;
-  private final int[] mTmp;
-  private final int LEN = 4;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ScrambleState(int[][] axis)
-    {
-    mTmp = new int[LEN];
-
-    mNumAxis = axis.length;
-    mNum = new int[mNumAxis];
-    int total =0;
-
-    for(int i=0; i<mNumAxis; i++)
-      {
-      mNum[i] = axis[i]==null ? 0 : axis[i].length/(LEN-1);
-      total += mNum[i];
-      }
-
-    mTotal = total;
-
-    mInfo = new int[LEN*total];
-    int start = 0;
-
-    for(int i=0; i<mNumAxis; i++)
-      {
-      for(int j=0; j<mNum[i]; j++)
-        {
-        mInfo[LEN*j   + start] = i;
-        mInfo[LEN*j+1 + start] = axis[i][(LEN-1)*j  ];
-        mInfo[LEN*j+2 + start] = axis[i][(LEN-1)*j+1];
-        mInfo[LEN*j+3 + start] = axis[i][(LEN-1)*j+2];
-        }
-
-      start += LEN*mNum[i];
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getIndex(int num, int indexExcluded)
-    {
-    int current= -1;
-
-    for(int i=0; i<mTotal; i++)
-      if( mInfo[LEN*i]!=indexExcluded && ++current==num ) return i;
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getRandom(Random rnd, int indexExcluded, int[][] scrambleTable, int[] numOccurences)
-    {
-    int num=0, total=0, max=0, ax, layer;
-
-    for(int i=0; i<mTotal; i++)
-      {
-      ax = mInfo[LEN*i];
-
-      if( ax!=indexExcluded )
-        {
-        layer = mInfo[LEN*i+1];
-        int value = scrambleTable[ax][layer];
-        if( value>max ) max=value;
-        }
-      }
-
-    for(int i=0; i<mTotal; i++)
-      {
-      ax = mInfo[LEN*i];
-
-      if( ax!=indexExcluded )
-        {
-        layer = mInfo[LEN*i+1];
-        int value = scrambleTable[ax][layer];
-        numOccurences[total] = 1 + max - value + (total==0 ? 0 : numOccurences[total-1]);
-        total++;
-        }
-      }
-
-    float random= rnd.nextFloat()*numOccurences[total-1];
-
-    for(int i=0; i<total; i++)
-      {
-      if( random <= numOccurences[i] )
-        {
-        num=i;
-        break;
-        }
-      }
-
-    int index = getIndex(num,indexExcluded);
-
-    mTmp[0] = mInfo[LEN*index  ];   // axis
-    mTmp[1] = mInfo[LEN*index+1];   // row
-    mTmp[2] = mInfo[LEN*index+2];   // angle
-    mTmp[3] = mInfo[LEN*index+3];   // next state
-
-    scrambleTable[mTmp[0]][mTmp[1]]++;
-
-    return mTmp;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getTotal(int indexExcluded)
-    {
-    return ( indexExcluded>=0 && indexExcluded<mNumAxis ) ? mTotal-mNum[indexExcluded] : mTotal;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/helpers/ScrambleStateBandagedEvil.java b/src/main/java/org/distorted/helpers/ScrambleStateBandagedEvil.java
deleted file mode 100644
index 4f6055d7..00000000
--- a/src/main/java/org/distorted/helpers/ScrambleStateBandagedEvil.java
+++ /dev/null
@@ -1,669 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// producer of the ScrambleStateGraph - but only for a single Twisty Puzzle, the 'BandagedEvil'.
-
-import java.util.ArrayList;
-
-public class ScrambleStateBandagedEvil
-{
-  private static final int INVALID_MOVE = -1;
-
-  private static final int CORNER_S = 0;
-  private static final int CORNER_X = 1;
-  private static final int CORNER_Y = 2;
-  private static final int CORNER_Z = 3;
-
-  private static final int CENTER_0 = 0;
-  private static final int CENTER_1 = 1;
-  private static final int CENTER_2 = 2;
-  private static final int CENTER_3 = 3;
-
-  private int mID;
-  private final int[] mMoves;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ScrambleStateBandagedEvil(int id)
-    {
-    mID = id;
-    mMoves = createMoves(mID);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void computeGraph()
-    {
-    ArrayList<ScrambleStateBandagedEvil> graph;
-
-    int id = 0;
-    int id1 = setCenter(id  , CENTER_2, 0);
-    int id2 = setCenter(id1 , CENTER_2, 1);
-    int id3 = setCenter(id2 , CENTER_3, 2);
-    int id4 = setCenter(id3 , CENTER_2, 3);
-
-    int id5 = setCorner(id4 , CORNER_X, 0);
-    int id6 = setCorner(id5 , CORNER_Y, 1);
-    int id7 = setCorner(id6 , CORNER_X, 2);
-    int id8 = setCorner(id7 , CORNER_Z, 3);
-    int id9 = setCorner(id8 , CORNER_Y, 4);
-    int id10= setCorner(id9 , CORNER_Y, 5);
-    int id11= setCorner(id10, CORNER_S, 6);
-    int id12= setCorner(id11, CORNER_Z, 7);
-
-    ScrambleStateBandagedEvil bsg = new ScrambleStateBandagedEvil(id12);
-    graph = new ArrayList<>();
-    graph.add(bsg);
-
-    insertChildren(graph,id12);
-    pruneGraph(graph);
-    remapGraph(graph);
-
-    int num = graph.size();
-    android.util.Log.e("D", "\n"+num+" states\n");
-
-    for(int i=0; i<num; i++)
-      {
-      bsg = graph.get(i);
-      android.util.Log.e("D", formatMoves(bsg));
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void insertChildren(ArrayList<ScrambleStateBandagedEvil> list, int id)
-    {
-    ScrambleStateBandagedEvil bsg = findState(list,id);
-
-    if( bsg==null )
-      {
-      android.util.Log.e("D", "error: "+id+" doesn't exist");
-      return;
-      }
-
-    for(int i=0; i<12; i++)
-      {
-      int move = bsg.getMove(i);
-
-      if( move!=INVALID_MOVE )
-        {
-        ScrambleStateBandagedEvil tmp = findState(list,move);
-
-        if( tmp==null )
-          {
-          tmp = new ScrambleStateBandagedEvil(move);
-          list.add(tmp);
-          insertChildren(list,move);
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void pruneGraph(ArrayList<ScrambleStateBandagedEvil> list)
-    {
-    int num = list.size(), numAxis;
-    boolean pruned = false;
-    ScrambleStateBandagedEvil bsg;
-
-    for(int i=0; i<num; i++)
-      {
-      bsg = list.get(i);
-      numAxis = bsg.numAxis();
-
-      if( numAxis<2 )
-        {
-        list.remove(i);
-        int id = bsg.getID();
-        pruned = true;
-        remapID(list,id,INVALID_MOVE);
-        break;
-        }
-      }
-
-    if( pruned ) pruneGraph(list);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void remapGraph(ArrayList<ScrambleStateBandagedEvil> list)
-    {
-    int id, num = list.size();
-    ScrambleStateBandagedEvil bsg;
-
-    for(int i=0; i<num; i++ )
-      {
-      bsg = list.get(i);
-      id = bsg.getID();
-      bsg.setID(i);
-      remapID(list,id,i);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void remapID(ArrayList<ScrambleStateBandagedEvil> list, int id, int newId)
-    {
-    ScrambleStateBandagedEvil bsg;
-    int size = list.size();
-
-    for(int i=0; i<size; i++)
-      {
-      bsg = list.get(i);
-
-      for(int j=0; j<12; j++)
-        {
-        if( bsg.getMove(j)==id ) bsg.setMove(j,newId);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static ScrambleStateBandagedEvil findState(ArrayList<ScrambleStateBandagedEvil> list, int id)
-    {
-    ScrambleStateBandagedEvil bsg;
-    int num = list.size();
-
-    for(int i=0; i<num; i++)
-      {
-      bsg= list.get(i);
-      if( bsg.getID() == id ) return bsg;
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String formatMoves(ScrambleStateBandagedEvil bsg)
-    {
-    String x = getTable(bsg,0);
-    String y = getTable(bsg,3);
-    String z = getTable(bsg,6);
-
-    return "    new ScrambleStateGraph( new int[][] { "+x+", "+y+", "+z+" } ),";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String getTable(ScrambleStateBandagedEvil sc, int index)
-    {
-    String ret = "";
-
-    if( index==0 || index==3 )
-      {
-      int m0 = sc.getMove(index  );
-      int m1 = sc.getMove(index+1);
-      int m2 = sc.getMove(index+2);
-
-      if( m0==INVALID_MOVE && m1==INVALID_MOVE && m2==INVALID_MOVE ) return formatL("{}");
-
-      if( m0!=INVALID_MOVE ) ret += formatRet(ret,2, 1,m0);
-      if( m1!=INVALID_MOVE ) ret += formatRet(ret,2, 2,m1);
-      if( m2!=INVALID_MOVE ) ret += formatRet(ret,2,-1,m2);
-      }
-    else
-      {
-      int m0 = sc.getMove(index  );
-      int m1 = sc.getMove(index+1);
-      int m2 = sc.getMove(index+2);
-      int m3 = sc.getMove(index+3);
-      int m4 = sc.getMove(index+4);
-      int m5 = sc.getMove(index+5);
-
-      if( m0==INVALID_MOVE && m1==INVALID_MOVE && m2==INVALID_MOVE &&
-          m3==INVALID_MOVE && m4==INVALID_MOVE && m5==INVALID_MOVE  )
-        {
-        return formatL("{}");
-        }
-
-      if( m0!=INVALID_MOVE ) ret += formatRet(ret,0, 1,m0);
-      if( m1!=INVALID_MOVE ) ret += formatRet(ret,0, 2,m1);
-      if( m2!=INVALID_MOVE ) ret += formatRet(ret,0,-1,m2);
-      if( m3!=INVALID_MOVE ) ret += formatRet(ret,2, 1,m3);
-      if( m4!=INVALID_MOVE ) ret += formatRet(ret,2, 2,m4);
-      if( m5!=INVALID_MOVE ) ret += formatRet(ret,2,-1,m5);
-      }
-
-    return formatL("{" + ret + "}");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String formatRet(String str, int row, int angle, int id)
-    {
-    String ret = str.length()!=0 ? ",":"";
-
-    ret += row;
-    ret += angle<0 ? "," : ", ";
-    ret += angle;
-
-         if( id< 10 ) ret += (",  "+id);
-    else if( id<100 ) ret += (", " +id);
-    else              ret += (","  +id);
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static final int LENGTH = 28;
-
-  private static String formatL(String input)
-    {
-    int len = input.length();
-    String ret = input;
-    for(int i=0 ;i<LENGTH-len; i++) ret += " ";
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getID()
-    {
-    return mID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setID(int id)
-    {
-    mID = id;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getMove(int index)
-    {
-    return (index>=0 && index<12) ? mMoves[index] : -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int numAxis()
-    {
-    int num = 0;
-
-    if( mMoves[ 0]!=INVALID_MOVE || mMoves[ 1]!=INVALID_MOVE || mMoves[ 2]!=INVALID_MOVE ) num++;
-    if( mMoves[ 3]!=INVALID_MOVE || mMoves[ 4]!=INVALID_MOVE || mMoves[ 5]!=INVALID_MOVE ) num++;
-    if( mMoves[ 6]!=INVALID_MOVE || mMoves[ 7]!=INVALID_MOVE || mMoves[ 8]!=INVALID_MOVE ) num++;
-    if( mMoves[ 9]!=INVALID_MOVE || mMoves[10]!=INVALID_MOVE || mMoves[11]!=INVALID_MOVE ) num++;
-
-    return num;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setMove(int index, int newMove)
-    {
-    if( index>=0 && index<12 ) mMoves[index] = newMove;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String debug(int id)
-    {
-    int ce0 = getCenter(id,0);
-    int ce1 = getCenter(id,1);
-    int ce2 = getCenter(id,2);
-    int ce3 = getCenter(id,3);
-
-    int co0 = getCorner(id,0);
-    int co1 = getCorner(id,1);
-    int co2 = getCorner(id,2);
-    int co3 = getCorner(id,3);
-    int co4 = getCorner(id,4);
-    int co5 = getCorner(id,5);
-    int co6 = getCorner(id,6);
-    int co7 = getCorner(id,7);
-
-    String center = centerString(ce0) + centerString(ce1) + centerString(ce2) + centerString(ce3);
-    String corner = cornerString(co0) + cornerString(co1) + cornerString(co2) + cornerString(co3) +
-                    cornerString(co4) + cornerString(co5) + cornerString(co6) + cornerString(co7);
-
-    return center + " -" + corner;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String centerString(int center)
-    {
-    return " "+center;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String cornerString(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return " S";
-      case CORNER_X: return " X";
-      case CORNER_Y: return " Y";
-      case CORNER_Z: return " Z";
-      }
-
-    return "?";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int[] createMoves(int id)
-    {
-    int[] ret = new int[12];
-
-    boolean moveX  = xPossible(id);
-    boolean moveY  = yPossible(id);
-    boolean moveZ0 = z0Possible(id);
-    boolean moveZ2 = z2Possible(id);
-
-    if( moveX ) createXmoves(id,ret);
-    else        { ret[ 0] = INVALID_MOVE; ret[ 1] = INVALID_MOVE; ret[ 2] = INVALID_MOVE; }
-    if( moveY ) createYmoves(id,ret);
-    else        { ret[ 3] = INVALID_MOVE; ret[ 4] = INVALID_MOVE; ret[ 5] = INVALID_MOVE; }
-    if( moveZ0) createZ0moves(id,ret);
-    else        { ret[ 6] = INVALID_MOVE; ret[ 7] = INVALID_MOVE; ret[ 8] = INVALID_MOVE; }
-    if( moveZ2) createZ2moves(id,ret);
-    else        { ret[ 9] = INVALID_MOVE; ret[10] = INVALID_MOVE; ret[11] = INVALID_MOVE; }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean xPossible(int id)
-    {
-    if( getCorner(id,4)==CORNER_X ) return false;
-    if( getCorner(id,5)==CORNER_X ) return false;
-    if( getCorner(id,6)==CORNER_X ) return false;
-    if( getCorner(id,7)==CORNER_X ) return false;
-
-    if( getCenter(id,1)==CENTER_1 ) return false;
-    if( getCenter(id,2)==CENTER_1 ) return false;
-    if( getCenter(id,3)==CENTER_1 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean yPossible(int id)
-    {
-    if( getCorner(id,2)==CORNER_Y ) return false;
-    if( getCorner(id,3)==CORNER_Y ) return false;
-    if( getCorner(id,6)==CORNER_Y ) return false;
-    if( getCorner(id,7)==CORNER_Y ) return false;
-
-    if( getCenter(id,0)==CENTER_0 ) return false;
-    if( getCenter(id,2)==CENTER_0 ) return false;
-    if( getCenter(id,3)==CENTER_0 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean z0Possible(int id)
-    {
-    if( getCorner(id,0)==CORNER_Z ) return false;
-    if( getCorner(id,2)==CORNER_Z ) return false;
-    if( getCorner(id,4)==CORNER_Z ) return false;
-    if( getCorner(id,6)==CORNER_Z ) return false;
-
-    if( getCenter(id,0)==CENTER_1 ) return false;
-    if( getCenter(id,1)==CENTER_0 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean z2Possible(int id)
-    {
-    if( getCorner(id,1)==CORNER_Z ) return false;
-    if( getCorner(id,3)==CORNER_Z ) return false;
-    if( getCorner(id,5)==CORNER_Z ) return false;
-    if( getCorner(id,7)==CORNER_Z ) return false;
-
-    if( getCenter(id,0)==CENTER_3 ) return false;
-    if( getCenter(id,1)==CENTER_2 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int getCorner(int id, int index)
-    {
-    return (id>>(14-2*index))&3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int getCenter(int id, int index)
-    {
-    return (id>>(22-2*index))&3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int setCorner(int id, int corner, int index)
-    {
-    return id + ((corner-getCorner(id,index))<<(14-2*index));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int setCenter(int id, int center, int index)
-    {
-    return id + ((center-getCenter(id,index))<<(22-2*index));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createXmoves(int id, int[] moves)
-    {
-    int id1 = rotateX(id);
-    moves[0] = id1;
-    int id2 = rotateX(id1);
-    moves[1] = id2;
-    int id3 = rotateX(id2);
-    moves[2] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createYmoves(int id, int[] moves)
-    {
-    int id1 = rotateY(id);
-    moves[3] = id1;
-    int id2 = rotateY(id1);
-    moves[4] = id2;
-    int id3 = rotateY(id2);
-    moves[5] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createZ0moves(int id, int[] moves)
-    {
-    int id1 = rotateZ0(id);
-    moves[6] = id1;
-    int id2 = rotateZ0(id1);
-    moves[7] = id2;
-    int id3 = rotateZ0(id2);
-    moves[8] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createZ2moves(int id, int[] moves)
-    {
-    int id1 = rotateZ2(id);
-    moves[ 9] = id1;
-    int id2 = rotateZ2(id1);
-    moves[10] = id2;
-    int id3 = rotateZ2(id2);
-    moves[11] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateX(int id)
-    {
-    int newCorner4 = rotCornerX(getCorner(id,5));
-    int newCorner5 = rotCornerX(getCorner(id,7));
-    int newCorner6 = rotCornerX(getCorner(id,4));
-    int newCorner7 = rotCornerX(getCorner(id,6));
-    int newCenter  = rotCenter (getCenter(id,0));
-
-    int id1 = setCorner(id ,newCorner4,4);
-    int id2 = setCorner(id1,newCorner5,5);
-    int id3 = setCorner(id2,newCorner6,6);
-    int id4 = setCorner(id3,newCorner7,7);
-    int id5 = setCenter(id4,newCenter ,0);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateY(int id)
-    {
-    int newCorner2 = rotCornerY(getCorner(id,6));
-    int newCorner3 = rotCornerY(getCorner(id,2));
-    int newCorner6 = rotCornerY(getCorner(id,7));
-    int newCorner7 = rotCornerY(getCorner(id,3));
-    int newCenter  = rotCenter (getCenter(id,1));
-
-    int id1 = setCorner(id ,newCorner2,2);
-    int id2 = setCorner(id1,newCorner3,3);
-    int id3 = setCorner(id2,newCorner6,6);
-    int id4 = setCorner(id3,newCorner7,7);
-    int id5 = setCenter(id4,newCenter ,1);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateZ0(int id)
-    {
-    int newCorner0 = rotCornerZ(getCorner(id,2));
-    int newCorner2 = rotCornerZ(getCorner(id,6));
-    int newCorner4 = rotCornerZ(getCorner(id,0));
-    int newCorner6 = rotCornerZ(getCorner(id,4));
-    int newCenter  = rotCenter (getCenter(id,2));
-
-    int id1 = setCorner(id ,newCorner0,0);
-    int id2 = setCorner(id1,newCorner2,2);
-    int id3 = setCorner(id2,newCorner4,4);
-    int id4 = setCorner(id3,newCorner6,6);
-    int id5 = setCenter(id4,newCenter ,2);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateZ2(int id)
-    {
-    int newCorner1 = rotCornerZ(getCorner(id,3));
-    int newCorner3 = rotCornerZ(getCorner(id,7));
-    int newCorner5 = rotCornerZ(getCorner(id,1));
-    int newCorner7 = rotCornerZ(getCorner(id,5));
-    int newCenter  = rotCenter (getCenter(id,3));
-
-    int id1 = setCorner(id ,newCorner1,1);
-    int id2 = setCorner(id1,newCorner3,3);
-    int id3 = setCorner(id2,newCorner5,5);
-    int id4 = setCorner(id3,newCorner7,7);
-    int id5 = setCenter(id4,newCenter ,3);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCornerX(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return CORNER_S;
-      case CORNER_X: android.util.Log.e("DIST", "rotateX: ERROR");
-                     return CORNER_S;
-      case CORNER_Y: return CORNER_Z;
-      case CORNER_Z: return CORNER_Y;
-      }
-
-    return CORNER_S;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCornerY(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return CORNER_S;
-      case CORNER_X: return CORNER_Z;
-      case CORNER_Y: android.util.Log.e("DIST", "rotateY: ERROR");
-                     return CORNER_S;
-      case CORNER_Z: return CORNER_X;
-      }
-
-    return CORNER_S;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCornerZ(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return CORNER_S;
-      case CORNER_X: return CORNER_Y;
-      case CORNER_Y: return CORNER_X;
-      case CORNER_Z: android.util.Log.e("DIST", "rotateZ: ERROR");
-                     return CORNER_S;
-      }
-
-    return CORNER_S;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCenter(int center)
-    {
-    switch(center)
-      {
-      case CENTER_0: return CENTER_3;
-      case CENTER_1: return CENTER_0;
-      case CENTER_2: return CENTER_1;
-      case CENTER_3: return CENTER_2;
-      }
-
-    return CENTER_0;
-    }
-}
diff --git a/src/main/java/org/distorted/helpers/ScrambleStateSquare1.java b/src/main/java/org/distorted/helpers/ScrambleStateSquare1.java
deleted file mode 100644
index 3fb87092..00000000
--- a/src/main/java/org/distorted/helpers/ScrambleStateSquare1.java
+++ /dev/null
@@ -1,594 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-import java.util.ArrayList;
-import java.util.Random;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// producer of the ScrambleStateGraph - but only for a single Twisty Puzzle, the 'Square-1'.
-
-public class ScrambleStateSquare1
-{
-  private static final int INVALID_MOVE = -1;
-  private static final int NUM_MOVES = 23;
-
-  private int mDist;
-  private boolean mFresh;
-  private long mID;
-  private final long[] mMoves;
-
-  private final int[][] mPermittedAngles;
-  private final int[] mCornerQuat, mTmp;
-
-  // QUATS[i]*QUATS[j] = QUATS[QUAT_MULT[i][j]]
-  static final int[][] QUAT_MULT = new int[][]
-    {
-      {  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,},
-      {  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12,},
-      {  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13,},
-      {  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14,},
-      {  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15,},
-      {  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16,},
-      {  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17,},
-      {  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18,},
-      {  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19,},
-      {  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20,},
-      { 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,},
-      { 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,},
-      { 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,},
-      { 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,},
-      { 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,},
-      { 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,},
-      { 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,},
-      { 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,},
-      { 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,},
-      { 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,},
-      { 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,},
-      { 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,},
-      { 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11,},
-      { 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0,}
-    };
-
-  // quat indices that make corner cubits bandage the puzzle.
-  private static final int[][] BAD_CORNER_QUATS = new int[][]
-    {
-      { 2, 8,17,23},
-      { 5,11,14,20},
-    };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ScrambleStateSquare1(long id, int dist)
-    {
-    mCornerQuat = new int[8];
-    mTmp        = new int[8];
-    mPermittedAngles = new int[2][12];
-
-    mDist = dist;
-    mFresh = true;
-    mID = id;
-    mMoves = createMoves(mID);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setNotFresh()
-    {
-    mFresh = false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean isFresh()
-    {
-    return mFresh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void computeGraph()
-    {
-    ArrayList<ScrambleStateSquare1> graph;
-
-    ScrambleStateSquare1 bsg = new ScrambleStateSquare1(0,0);
-    graph = new ArrayList<>();
-    graph.add(bsg);
-
-android.util.Log.e("D", "INSERTING");
-
-    insertChildren(graph,0);
-
-//android.util.Log.e("D", "PRUNING "+graph.size());
-
-//    pruneGraph(graph);
-
-android.util.Log.e("D", "REMAPPING "+graph.size());
-
-    remapGraph(graph);
-
-    int num = graph.size();
-    android.util.Log.e("D", "\n"+num+" states\n");
-
-    for(int i=0; i<num; i++)
-      {
-      bsg = graph.get(i);
-      android.util.Log.e("D", formatMoves(bsg));
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void insertChildren(ArrayList<ScrambleStateSquare1> list, long id)
-    {
-    ScrambleStateSquare1 bsg = findState(list,id);
-
-    if( bsg==null )
-      {
-      android.util.Log.e("D", "error: "+id+" doesn't exist");
-      return;
-      }
-
-    int dist = bsg.mDist;
-    if( dist>6 ) return;
-
-    for(int i=0; i<NUM_MOVES; i++)
-      {
-      long moveID = bsg.getMoveID(i);
-
-      if( moveID!=INVALID_MOVE )
-        {
-        ScrambleStateSquare1 tmp = findState(list,moveID);
-
-        if( tmp==null )
-          {
-          tmp = new ScrambleStateSquare1(moveID,dist+1);
-          list.add(tmp);
-          }
-        }
-      }
-
-    for(int i=0; i<NUM_MOVES; i++)
-      {
-      long moveID = bsg.getMoveID(i);
-
-      if( moveID!=INVALID_MOVE )
-        {
-        ScrambleStateSquare1 tmp = findState(list,moveID);
-
-        if( tmp.isFresh() )
-          {
-          tmp.setNotFresh();
-          insertChildren(list,moveID);
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void pruneGraph(ArrayList<ScrambleStateSquare1> list)
-    {
-    int num = list.size(), numAxis;
-    boolean pruned = false;
-    ScrambleStateSquare1 bsg;
-
-    for(int i=0; i<num; i++)
-      {
-      bsg = list.get(i);
-      numAxis = bsg.numAxis();
-
-      if( numAxis<2 )
-        {
-        list.remove(i);
-        long id = bsg.getID();
-        pruned = true;
-        remapID(list,id,INVALID_MOVE);
-        break;
-        }
-      }
-
-    if( pruned ) pruneGraph(list);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void remapGraph(ArrayList<ScrambleStateSquare1> list)
-    {
-    long id;
-    int num = list.size();
-    ScrambleStateSquare1 bsg;
-
-    for(int i=0; i<num; i++ )
-      {
-      bsg = list.get(i);
-      id = bsg.getID();
-      bsg.setID(i);
-      remapID(list,id,i);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void remapID(ArrayList<ScrambleStateSquare1> list, long id, long newId)
-    {
-    ScrambleStateSquare1 bsg;
-    int size = list.size();
-
-    for(int i=0; i<size; i++)
-      {
-      bsg = list.get(i);
-
-      for(int j=0; j<NUM_MOVES; j++)
-        {
-        if( bsg.getMoveID(j)==id ) bsg.setMoveID(j,newId);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static ScrambleStateSquare1 findState(ArrayList<ScrambleStateSquare1> list, long id)
-    {
-    ScrambleStateSquare1 bsg;
-    int num = list.size();
-
-    for(int i=0; i<num; i++)
-      {
-      bsg= list.get(i);
-      if( bsg.getID() == id ) return bsg;
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String formatMoves(ScrambleStateSquare1 bsg)
-    {
-    String x = getTable(bsg,0);
-    String y = getTable(bsg,11);
-    String z = getTable(bsg,12);
-
-    //return "    new ScrambleStateGraph( new int[][] { "+x+", "+y+", "+z+" } ),";
-
-    long id = bsg.getID();
-
-    return id+" "+x+" , "+y+" , "+z;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String getTable(ScrambleStateSquare1 sc, int index)
-    {
-    String ret = "";
-    long m;
-    int half = (NUM_MOVES)/2;
-
-    if( index==11 )
-      {
-      m = sc.getMoveID(index);
-      if( m==INVALID_MOVE ) return formatL("{}");
-      ret += formatRet(ret,0,1,m);
-      }
-    else
-      {
-      for(int i=0; i<half; i++)
-        {
-        m = sc.getMoveID(index+i);
-        int row = index==0 ? 0:2;
-        int angle = i+1;
-        if( m!=INVALID_MOVE ) ret += formatRet(ret,row,angle,m);
-        }
-      }
-
-    return formatL("{" + ret + "}");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String formatRet(String str, int row, int angle, long id)
-    {
-    String ret = str.length()!=0 ? "  ,":"  ";
-
-    ret += row;
-    ret += angle<0 ? "," : ",";
-    ret += angle;
-
-         if( id< 10 ) ret += (",  "+id);
-    else if( id<100 ) ret += (", " +id);
-    else              ret += (","  +id);
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static final int LENGTH = 28;
-
-  private static String formatL(String input)
-    {
-    int len = input.length();
-    String ret = input;
-    for(int i=0 ;i<LENGTH-len; i++) ret += " ";
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private long getID()
-    {
-    return mID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setID(long id)
-    {
-    mID = id;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private long getMoveID(int index)
-    {
-    return (index>=0 && index<NUM_MOVES) ? mMoves[index] : -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setMoveID(int index, long newID)
-    {
-    if( index>=0 && index<NUM_MOVES ) mMoves[index] = newID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int numAxis()
-    {
-    int num = 0;
-    int half = (NUM_MOVES)/2;
-
-    for(int i=0; i<half; i++)
-      {
-      if( mMoves[i]!=INVALID_MOVE )
-        {
-        num++;
-        break;
-        }
-      }
-
-    for(int i=half; i<NUM_MOVES-1; i++)
-      {
-      if( mMoves[i]!=INVALID_MOVE )
-        {
-        num++;
-        break;
-        }
-      }
-
-    if( mMoves[NUM_MOVES-1]!=INVALID_MOVE ) num++;
-
-    return num;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private long[] createMoves(long id)
-    {
-    long[] ret = new long[NUM_MOVES];
-    fillCornerQuat(id);
-    computePermittedAngles();
-
-    generateUMoves(ret, 0);
-    generateSMoves(ret,11);
-    generateLMoves(ret,12);
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void generateUMoves(long[] table, int index)
-    {
-    for(int angle=1; angle<12; angle++)
-      {
-      table[index+angle-1] = mPermittedAngles[1][angle]==1 ? updateCornerQuats(0,2,angle) : INVALID_MOVE;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void generateLMoves(long[] table, int index)
-    {
-    for(int angle=1; angle<12; angle++)
-      {
-      table[index+angle-1] = mPermittedAngles[0][angle]==1 ? updateCornerQuats(0,0,angle) : INVALID_MOVE;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void generateSMoves(long[] table, int index)
-    {
-    table[index] = updateCornerQuats(1,1,1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void fillCornerQuat(long id)
-    {
-    int NUM_QUATS = 24;
-
-    for(int i=0; i<8; i++)
-      {
-      mCornerQuat[i] = (int)(id%NUM_QUATS);
-      id/=NUM_QUATS;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static long returnID(int[] cornerQuat)
-    {
-    int NUM_QUATS = 24;
-    long mult=1,ret=0;
-
-    for(int i=0; i<8; i++)
-      {
-      ret += mult*cornerQuat[i];
-      mult*=NUM_QUATS;
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean cornerIsUp(int index)
-    {
-    return ((index<4) ^ (mCornerQuat[index]>=12));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean cornerIsLeft(int index)
-    {
-    int q = mCornerQuat[index];
-
-    switch(index)
-      {
-      case 0:
-      case 4: return ((q>=3 && q<= 7) || (q>=18 && q<=22));
-      case 1:
-      case 5: return ((q>=6 && q<=10) || (q>=15 && q<=19));
-      case 2:
-      case 6: return ((q==0 || q==1 || (q>=9 && q<=11)) || (q>=12 && q<=16));
-      case 3:
-      case 7: return ((q>=0 && q<=4) || (q==12 || q==13 || (q>=21 && q<=23)));
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int makeQuat(int axis,int index)
-    {
-    if( axis==1 ) return 13;
-    if( index<0 ) index+=12;
-    return index;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean cornerBelongs(int index, int axis, int layer)
-    {
-    if( axis==0 )
-      {
-      boolean up = cornerIsUp(index);
-      return ((up && layer==2) || (!up && layer==0));
-      }
-    else
-      {
-      boolean le = cornerIsLeft(index);
-      return ((le && layer==0) || (!le && layer==1));
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private long updateCornerQuats(int axis, int layer, int index)
-    {
-    int quat = makeQuat(axis,index);
-
-    for(int corner=0; corner<8; corner++)
-      {
-      if( cornerBelongs(corner,axis,layer) )
-        {
-        int curr = mCornerQuat[corner];
-        mTmp[corner] = QUAT_MULT[quat][curr];
-        }
-      else
-        {
-        mTmp[corner] = mCornerQuat[corner];
-        }
-      }
-
-    return returnID(mTmp);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean quatIsBad(int quatIndex, int corner)
-    {
-    int index = (corner%2);
-
-    return ( quatIndex==BAD_CORNER_QUATS[index][0] ||
-             quatIndex==BAD_CORNER_QUATS[index][1] ||
-             quatIndex==BAD_CORNER_QUATS[index][2] ||
-             quatIndex==BAD_CORNER_QUATS[index][3]  );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int isPermittedDo(int angle)
-    {
-    for(int corner=0; corner<8; corner++)
-      {
-      if( !cornerIsUp(corner) )
-        {
-        int currQuat = mCornerQuat[corner];
-        int finalQuat= QUAT_MULT[angle][currQuat];
-        if( quatIsBad(finalQuat,corner) ) return 0;
-        }
-      }
-
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int isPermittedUp(int angle)
-    {
-    for(int corner=0; corner<8; corner++)
-      {
-      if( cornerIsUp(corner) )
-        {
-        int currQuat = mCornerQuat[corner];
-        int finalQuat= QUAT_MULT[angle][currQuat];
-        if( quatIsBad(finalQuat,corner) ) return 0;
-        }
-      }
-
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void computePermittedAngles()
-    {
-    for(int angle=0; angle<12; angle++)
-      {
-      mPermittedAngles[0][angle] = isPermittedDo(angle);
-      mPermittedAngles[1][angle] = isPermittedUp(angle);
-      }
-    }
-}
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index 12737af3..c52af4fb 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -45,10 +45,10 @@ import org.distorted.library.main.DistortedLibrary;
 
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.type.Static4D;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 import org.distorted.network.RubikScores;
 import org.distorted.network.RubikNetwork;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.screens.ScreenList;
 import org.distorted.screens.RubikScreenPlay;
 import org.distorted.tutorials.TutorialActivity;
diff --git a/src/main/java/org/distorted/main/RubikPreRender.java b/src/main/java/org/distorted/main/RubikPreRender.java
index 2b33d308..f4145399 100644
--- a/src/main/java/org/distorted/main/RubikPreRender.java
+++ b/src/main/java/org/distorted/main/RubikPreRender.java
@@ -42,8 +42,8 @@ import org.distorted.effects.scramble.ScrambleEffect;
 import org.distorted.helpers.BlockController;
 import org.distorted.helpers.MovesFinished;
 import org.distorted.helpers.TwistyPreRender;
-import org.distorted.objects.TwistyObject;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.TwistyObject;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.network.RubikScores;
 import org.distorted.screens.RubikScreenPlay;
 import org.distorted.screens.ScreenList;
diff --git a/src/main/java/org/distorted/main/RubikSurfaceView.java b/src/main/java/org/distorted/main/RubikSurfaceView.java
index ca471e5f..abc90a05 100644
--- a/src/main/java/org/distorted/main/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/main/RubikSurfaceView.java
@@ -30,11 +30,11 @@ import android.view.MotionEvent;
 
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
-import org.distorted.helpers.QuatHelper;
+import org.distorted.objectlb.QuatHelper;
 import org.distorted.library.type.Static2D;
 import org.distorted.library.type.Static4D;
-import org.distorted.objects.TwistyObject;
-import org.distorted.objects.Movement;
+import org.distorted.objectlb.TwistyObject;
+import org.distorted.objectlb.Movement;
 import org.distorted.screens.RubikScreenReady;
 import org.distorted.solvers.SolverMain;
 import org.distorted.screens.ScreenList;
diff --git a/src/main/java/org/distorted/network/RubikNetwork.java b/src/main/java/org/distorted/network/RubikNetwork.java
index 855d1a26..a46d7544 100644
--- a/src/main/java/org/distorted/network/RubikNetwork.java
+++ b/src/main/java/org/distorted/network/RubikNetwork.java
@@ -27,7 +27,7 @@ import androidx.appcompat.app.AppCompatActivity;
 import androidx.fragment.app.FragmentActivity;
 
 import org.distorted.library.main.DistortedLibrary;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 
 import java.io.InputStream;
 import java.net.HttpURLConnection;
@@ -36,7 +36,7 @@ import java.net.UnknownHostException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
-import static org.distorted.objects.ObjectList.MAX_LEVEL;
+import static org.distorted.objectlb.ObjectList.MAX_LEVEL;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/network/RubikScores.java b/src/main/java/org/distorted/network/RubikScores.java
index a0e951b3..151230ec 100644
--- a/src/main/java/org/distorted/network/RubikScores.java
+++ b/src/main/java/org/distorted/network/RubikScores.java
@@ -26,13 +26,13 @@ import android.telephony.TelephonyManager;
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
 import org.distorted.main.BuildConfig;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 
 import java.util.UUID;
 
-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;
+import static org.distorted.objectlb.ObjectList.MAX_NUM_OBJECTS;
+import static org.distorted.objectlb.ObjectList.NUM_OBJECTS;
+import static org.distorted.objectlb.ObjectList.MAX_LEVEL;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // hold my own scores, and some other statistics.
diff --git a/src/main/java/org/distorted/objectlb/Cubit.java b/src/main/java/org/distorted/objectlb/Cubit.java
new file mode 100644
index 00000000..c76bbb95
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Cubit.java
@@ -0,0 +1,227 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import android.content.SharedPreferences;
+
+import org.distorted.library.type.Static4D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class Cubit
+  {
+  private final float[] mOrigPosition;
+  private final float[] mCurrentPosition;
+  private final int mNumAxis;
+  private final int mLen;
+  private final int[] mRotationRow;
+  private TwistyObject mParent;
+
+  int mQuatIndex;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Cubit(TwistyObject parent, float[] position, int numAxis)
+    {
+    mQuatIndex= 0;
+    mParent   = parent;
+    mLen      = position.length;
+
+    mOrigPosition    = new float[mLen];
+    mCurrentPosition = new float[mLen];
+
+    for(int i=0; i<mLen; i++)
+      {
+      mOrigPosition[i]    = position[i];
+      mCurrentPosition[i] = position[i];
+      }
+
+    mNumAxis     = numAxis;
+    mRotationRow = new int[mNumAxis];
+    computeRotationRow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Because of quatMultiplication, errors can accumulate - so to avoid this, we
+// correct the value of the 'scramble' quat to what it should be - one of the legal quats from the
+// list QUATS.
+//
+// We also have to remember that the group of unit quaternions is a double-cover of rotations
+// in 3D ( q represents the same rotation as -q ) - so invert if needed.
+
+  private int normalizeScrambleQuat(Static4D quat)
+    {
+    float x = quat.get0();
+    float y = quat.get1();
+    float z = quat.get2();
+    float w = quat.get3();
+    float xd,yd,zd,wd;
+    float diff, mindiff = Float.MAX_VALUE;
+    int ret=0;
+    int num_quats = mParent.OBJECT_QUATS.length;
+    Static4D qt;
+
+    for(int q=0; q<num_quats; q++)
+      {
+      qt = mParent.OBJECT_QUATS[q];
+
+      xd = x - qt.get0();
+      yd = y - qt.get1();
+      zd = z - qt.get2();
+      wd = w - qt.get3();
+
+      diff = xd*xd + yd*yd + zd*zd + wd*wd;
+
+      if( diff < mindiff )
+        {
+        ret = q;
+        mindiff = diff;
+        }
+
+      xd = x + qt.get0();
+      yd = y + qt.get1();
+      zd = z + qt.get2();
+      wd = w + qt.get3();
+
+      diff = xd*xd + yd*yd + zd*zd + wd*wd;
+
+      if( diff < mindiff )
+        {
+        ret = q;
+        mindiff = diff;
+        }
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computeRotationRow()
+    {
+    for(int i=0; i<mNumAxis; i++)
+      {
+      mRotationRow[i] = mParent.computeRow(mCurrentPosition,i);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void modifyCurrentPosition(Static4D quat)
+    {
+    Static4D cubitCenter;
+    Static4D rotatedCenter;
+    int len = mLen/3;
+
+    for(int i=0; i<len; i++)
+      {
+      cubitCenter =  new Static4D(mCurrentPosition[3*i], mCurrentPosition[3*i+1], mCurrentPosition[3*i+2], 0);
+      rotatedCenter = QuatHelper.rotateVectorByQuat( cubitCenter, quat);
+
+      mCurrentPosition[3*i  ] = rotatedCenter.get0();
+      mCurrentPosition[3*i+1] = rotatedCenter.get1();
+      mCurrentPosition[3*i+2] = rotatedCenter.get2();
+
+      mParent.clampPos(mCurrentPosition, 3*i);
+      }
+
+    computeRotationRow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int computeAssociation()
+    {
+    int result = 0, accumulativeShift = 0;
+
+    for(int axis=0; axis<mNumAxis; axis++)
+      {
+      result += (mRotationRow[axis]<<accumulativeShift);
+      accumulativeShift += ObjectList.MAX_OBJECT_SIZE;
+      }
+
+    return result;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void savePreferences(SharedPreferences.Editor editor)
+    {
+    String number = mOrigPosition[0]+"_"+mOrigPosition[1]+"_"+mOrigPosition[2];
+    editor.putInt("q_"+number, mQuatIndex);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int restorePreferences(SharedPreferences preferences)
+    {
+    String number = mOrigPosition[0]+"_"+mOrigPosition[1]+"_"+mOrigPosition[2];
+    mQuatIndex = preferences.getInt("q_"+number, 0);
+    return mQuatIndex;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int removeRotationNow(Static4D quat)
+    {
+    Static4D q = QuatHelper.quatMultiply(quat,mParent.OBJECT_QUATS[mQuatIndex]);
+    mQuatIndex = normalizeScrambleQuat(q);
+
+    modifyCurrentPosition(quat);
+
+    return mQuatIndex;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void solve()
+    {
+    mQuatIndex = 0;
+    System.arraycopy(mOrigPosition, 0, mCurrentPosition, 0, mCurrentPosition.length);
+    computeRotationRow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void releaseResources()
+    {
+    mParent = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this is only needed for MODE_REPLACE objects (i.e. - currently - CUBE_3), so it is enough to only
+// take into consideration the first position.
+
+  float getDistSquared(float[] point)
+    {
+    float dx = mCurrentPosition[0] - point[0];
+    float dy = mCurrentPosition[1] - point[1];
+    float dz = mCurrentPosition[2] - point[2];
+
+    return dx*dx + dy*dy + dz*dz;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getRotRow(int index)
+    {
+    return mRotationRow[index];
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/objectlb/FactoryCubit.java b/src/main/java/org/distorted/objectlb/FactoryCubit.java
new file mode 100644
index 00000000..5562c13c
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/FactoryCubit.java
@@ -0,0 +1,940 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import org.distorted.library.effect.MatrixEffectMove;
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.MatrixEffectScale;
+import org.distorted.library.effect.VertexEffect;
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class FactoryCubit
+  {
+  private static final Static1D RADIUS = new Static1D(1);
+  private static FactoryCubit mThis;
+
+  private static final double[] mBuffer = new double[3];
+  private static final double[] mQuat1  = new double[4];
+  private static final double[] mQuat2  = new double[4];
+  private static final double[] mQuat3  = new double[4];
+  private static final double[] mQuat4  = new double[4];
+
+  private static class StickerCoords
+    {
+    double[] vertices;
+    }
+
+  private static class FaceTransform
+    {
+    int sticker;
+    double vx,vy,vz;
+    double scale;
+    double qx,qy,qz,qw;
+    boolean flip;
+    }
+
+  private static final ArrayList<FaceTransform> mNewFaceTransf = new ArrayList<>();
+  private static final ArrayList<FaceTransform> mOldFaceTransf = new ArrayList<>();
+  private static final ArrayList<StickerCoords> mStickerCoords = new ArrayList<>();
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private FactoryCubit()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static FactoryCubit getInstance()
+    {
+    if( mThis==null ) mThis = new FactoryCubit();
+
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// H - height of the band in the middle
+// alpha - angle of the edge  [0,90]
+// dist - often in a polygon the distance from edge to center is not 1, but something else.
+// This is the distance.
+// K - where to begin the second, much more flat part of the band. [0,1]
+// N - number of bands. N>=3
+//
+// theory: two distinct parts to the band:
+// 1) (0,B) - steep
+// 2) (B,1) - flat
+//
+// In first part, we have y = g(x) ; in second - y = g(f(x)) where
+//
+// g(x) = sqrt( R^2 - (x-D)^2 ) - R*cos(alpha)
+// f(x) = ((D-B)/(1-B)*x + B*(1-D)/(1-B)
+// h(x) = R*(sin(alpha) - sin(x))
+// R = H/(1-cos(alpha))
+// D = H*sin(alpha)
+// B = h(K*alpha)
+//
+// The N points are taken at:
+//
+// 1) in the second part, there are K2 = (N-3)/3 such points
+// 2) in the first - K1 = (N-3) - K2
+// 3) also, the 3 points 0,B,1
+//
+// so we have the sequence A[i] of N points
+//
+// 0
+// h((i+1)*(1-K)*alpha/(K1+1)) (i=0,1,...,K1-1)
+// B
+// (1-B)*(i+1)/(K2+1) + B   (i=0,i,...,K2-1)
+// 1
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float f(float D, float B, float x)
+    {
+    return ((D-B)*x + B*(1-D))/(1-B);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float g(float R, float D, float x, float cosAlpha)
+    {
+    float d = x-D;
+    return (float)(Math.sqrt(R*R-d*d)-R*cosAlpha);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float h(float R, float sinAlpha, float x)
+    {
+    return R*(sinAlpha-(float)Math.sin(x));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean areColinear(double[][] vertices, int index1, int index2, int index3)
+    {
+    double x1 = vertices[index1][0];
+    double y1 = vertices[index1][1];
+    double z1 = vertices[index1][2];
+    double x2 = vertices[index2][0];
+    double y2 = vertices[index2][1];
+    double z2 = vertices[index2][2];
+    double x3 = vertices[index3][0];
+    double y3 = vertices[index3][1];
+    double z3 = vertices[index3][2];
+
+    double v1x = x2-x1;
+    double v1y = y2-y1;
+    double v1z = z2-z1;
+    double v2x = x3-x1;
+    double v2y = y3-y1;
+    double v2z = z3-z1;
+
+    double A = Math.sqrt( (v1x*v1x+v1y*v1y+v1z*v1z) / (v2x*v2x+v2y*v2y+v2z*v2z) );
+
+    return (v1x==A*v2x && v1y==A*v2y && v1z==A*v2z);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computeNormalVector(double[][] vertices, int index1, int index2, int index3)
+    {
+    double x1 = vertices[index1][0];
+    double y1 = vertices[index1][1];
+    double z1 = vertices[index1][2];
+    double x2 = vertices[index2][0];
+    double y2 = vertices[index2][1];
+    double z2 = vertices[index2][2];
+    double x3 = vertices[index3][0];
+    double y3 = vertices[index3][1];
+    double z3 = vertices[index3][2];
+
+    double v1x = x2-x1;
+    double v1y = y2-y1;
+    double v1z = z2-z1;
+    double v2x = x3-x1;
+    double v2y = y3-y1;
+    double v2z = z3-z1;
+
+    mBuffer[0] = v1y*v2z - v2y*v1z;
+    mBuffer[1] = v1z*v2x - v2z*v1x;
+    mBuffer[2] = v1x*v2y - v2x*v1y;
+
+    double len = mBuffer[0]*mBuffer[0] + mBuffer[1]*mBuffer[1] + mBuffer[2]*mBuffer[2];
+    len = Math.sqrt(len);
+    mBuffer[0] /= len;
+    mBuffer[1] /= len;
+    mBuffer[2] /= len;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return quat1*quat2
+
+  private static void quatMultiply( double[] quat1, double[] quat2, double[] result )
+    {
+    double qx = quat1[0];
+    double qy = quat1[1];
+    double qz = quat1[2];
+    double qw = quat1[3];
+
+    double rx = quat2[0];
+    double ry = quat2[1];
+    double rz = quat2[2];
+    double rw = quat2[3];
+
+    result[0] = rw*qx - rz*qy + ry*qz + rx*qw;
+    result[1] = rw*qy + rz*qx + ry*qw - rx*qz;
+    result[2] = rw*qz + rz*qw - ry*qx + rx*qy;
+    result[3] = rw*qw - rz*qz - ry*qy - rx*qx;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void fitInSquare(FaceTransform info, double[][] vert3D)
+    {
+    double minX = Double.MAX_VALUE;
+    double maxX =-Double.MAX_VALUE;
+    double minY = Double.MAX_VALUE;
+    double maxY =-Double.MAX_VALUE;
+
+    for (double[] vert : vert3D)
+      {
+      double x = vert[0];
+      double y = vert[1];
+
+      if (x > maxX) maxX = x;
+      if (x < minX) minX = x;
+      if (y > maxY) maxY = y;
+      if (y < minY) minY = y;
+      }
+
+    minX = minX<0 ? -minX:minX;
+    maxX = maxX<0 ? -maxX:maxX;
+    minY = minY<0 ? -minY:minY;
+    maxY = maxY<0 ? -maxY:maxY;
+
+    double max1 = Math.max(minX,minY);
+    double max2 = Math.max(maxX,maxY);
+    double max3 = Math.max(max1,max2);
+
+    info.scale = max3/0.5;
+
+    int len = vert3D.length;
+    StickerCoords sInfo = new StickerCoords();
+    sInfo.vertices = new double[2*len];
+
+    for( int vertex=0; vertex<len; vertex++ )
+      {
+      sInfo.vertices[2*vertex  ] = vert3D[vertex][0] / info.scale;
+      sInfo.vertices[2*vertex+1] = vert3D[vertex][1] / info.scale;
+      }
+
+    mStickerCoords.add(sInfo);
+
+    info.sticker = mStickerCoords.size() -1;
+    info.flip = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private FaceTransform constructNewTransform(final double[][] vert3D)
+    {
+    FaceTransform ft = new FaceTransform();
+
+    // compute center of gravity
+    ft.vx = 0.0f;
+    ft.vy = 0.0f;
+    ft.vz = 0.0f;
+    int len = vert3D.length;
+
+    for (double[] vert : vert3D)
+      {
+      ft.vx += vert[0];
+      ft.vy += vert[1];
+      ft.vz += vert[2];
+      }
+
+    ft.vx /= len;
+    ft.vy /= len;
+    ft.vz /= len;
+
+    // move all vertices so that their center of gravity is at (0,0,0)
+    for (int i=0; i<len; i++)
+      {
+      vert3D[i][0] -= ft.vx;
+      vert3D[i][1] -= ft.vy;
+      vert3D[i][2] -= ft.vz;
+      }
+
+    // find 3 non-colinear vertices
+    int foundIndex = -1;
+
+    for(int vertex=2; vertex<len; vertex++)
+      {
+      if( !areColinear(vert3D,0,1,vertex) )
+        {
+        foundIndex = vertex;
+        break;
+        }
+      }
+
+    // compute the normal vector
+    if( foundIndex==-1 )
+      {
+      throw new RuntimeException("all vertices colinear");
+      }
+
+    computeNormalVector(vert3D,0,1,foundIndex);
+
+    // rotate so that the normal vector becomes (0,0,1)
+    double axisX, axisY, axisZ;
+
+    if( mBuffer[0]!=0.0f || mBuffer[1]!=0.0f )
+      {
+      axisX = -mBuffer[1];
+      axisY =  mBuffer[0];
+      axisZ = 0.0f;
+
+      double axiLen = axisX*axisX + axisY*axisY;
+      axiLen = Math.sqrt(axiLen);
+      axisX /= axiLen;
+      axisY /= axiLen;
+      axisZ /= axiLen;
+      }
+    else
+      {
+      axisX = 0.0f;
+      axisY = 1.0f;
+      axisZ = 0.0f;
+      }
+
+    double cosTheta = mBuffer[2];
+    double sinTheta = Math.sqrt(1-cosTheta*cosTheta);
+    double sinHalfTheta = computeSinHalf(cosTheta);
+    double cosHalfTheta = computeCosHalf(sinTheta,cosTheta);
+
+    mQuat1[0] = axisX*sinHalfTheta;
+    mQuat1[1] = axisY*sinHalfTheta;
+    mQuat1[2] = axisZ*sinHalfTheta;
+    mQuat1[3] = cosHalfTheta;
+    mQuat2[0] =-axisX*sinHalfTheta;
+    mQuat2[1] =-axisY*sinHalfTheta;
+    mQuat2[2] =-axisZ*sinHalfTheta;
+    mQuat2[3] = cosHalfTheta;
+
+    for (double[] vert : vert3D)
+      {
+      quatMultiply(mQuat1, vert  , mQuat3);
+      quatMultiply(mQuat3, mQuat2, vert  );
+      }
+
+    // fit the whole thing in a square and remember the scale & 2D vertices
+    fitInSquare(ft, vert3D);
+
+    // remember the rotation
+    ft.qx =-mQuat1[0];
+    ft.qy =-mQuat1[1];
+    ft.qz =-mQuat1[2];
+    ft.qw = mQuat1[3];
+
+    return ft;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void rotateAllVertices(double[] result, int len, double[] vertices, double sin, double cos)
+    {
+    for(int i=0; i<len; i++)
+      {
+      result[2*i  ] = vertices[2*i  ]*cos - vertices[2*i+1]*sin;
+      result[2*i+1] = vertices[2*i  ]*sin + vertices[2*i+1]*cos;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private double computeScale(double[] v1, double[] v2, int v1i, int v2i)
+    {
+    double v1x = v1[2*v1i];
+    double v1y = v1[2*v1i+1];
+    double v2x = v2[2*v2i];
+    double v2y = v2[2*v2i+1];
+
+    double lenSq1 = v1x*v1x + v1y*v1y;
+    double lenSq2 = v2x*v2x + v2y*v2y;
+
+    return Math.sqrt(lenSq2/lenSq1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// valid for 0<angle<2*PI
+
+  private double computeSinHalf(double cos)
+    {
+    return Math.sqrt((1-cos)/2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// valid for 0<angle<2*PI
+
+  private double computeCosHalf(double sin, double cos)
+    {
+    double cosHalf = Math.sqrt((1+cos)/2);
+    return sin<0 ? -cosHalf : cosHalf;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeRotatedIndex(int oldVertex, int len, int rotatedVertex, boolean inverted)
+    {
+    int v = (rotatedVertex + (inverted? -oldVertex : oldVertex));
+    if( v>=len ) v-=len;
+    if( v< 0   ) v+=len;
+
+    return v;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean isScaledVersionOf(double[] newVert, double[] oldVert, int len, int vertex, boolean inverted)
+    {
+    int newZeroIndex = computeRotatedIndex(0,len,vertex,inverted);
+    double EPSILON = 0.001;
+    double scale = computeScale(newVert,oldVert,newZeroIndex,0);
+
+    for(int i=1; i<len; i++)
+      {
+      int index = computeRotatedIndex(i,len,vertex,inverted);
+
+      double horz = oldVert[2*i  ] - scale*newVert[2*index  ];
+      double vert = oldVert[2*i+1] - scale*newVert[2*index+1];
+
+      if( horz>EPSILON || horz<-EPSILON || vert>EPSILON || vert<-EPSILON ) return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void mirrorAllVertices(double[] output, int len, double[] input)
+    {
+    for(int vertex=0; vertex<len; vertex++)
+      {
+      output[2*vertex  ] = input[2*vertex  ];
+      output[2*vertex+1] =-input[2*vertex+1];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void correctInfo(FaceTransform info, double scale, double sin, double cos, int oldSticker, boolean flip)
+    {
+    mStickerCoords.remove(info.sticker);
+
+    info.flip    = flip;
+    info.sticker = oldSticker;
+    info.scale  *= scale;
+
+    mQuat1[0] = info.qx;
+    mQuat1[1] = info.qy;
+    mQuat1[2] = info.qz;
+    mQuat1[3] = info.qw;
+
+    double sinHalf = computeSinHalf(cos);
+    double cosHalf = computeCosHalf(sin,cos);
+
+    if( flip )
+      {
+      mQuat3[0] = 0.0f;
+      mQuat3[1] = 0.0f;
+      mQuat3[2] = sinHalf;
+      mQuat3[3] = cosHalf;
+
+      mQuat4[0] = 1.0;
+      mQuat4[1] = 0.0;
+      mQuat4[2] = 0.0;
+      mQuat4[3] = 0.0;
+
+      quatMultiply( mQuat3, mQuat4, mQuat2 );
+      }
+    else
+      {
+      mQuat2[0] = 0.0f;
+      mQuat2[1] = 0.0f;
+      mQuat2[2] = sinHalf;
+      mQuat2[3] = cosHalf;
+      }
+
+    quatMultiply( mQuat1, mQuat2, mQuat3 );
+
+    info.qx = mQuat3[0];
+    info.qy = mQuat3[1];
+    info.qz = mQuat3[2];
+    info.qw = mQuat3[3];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void printVert(double[] buffer)
+    {
+    int len = buffer.length/2;
+    String str = "";
+
+    for(int i=0; i<len; i++)
+      {
+      str += (" ("+buffer[2*i]+" , "+buffer[2*i+1]+" ) ");
+      }
+
+    android.util.Log.d("D", str);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean foundVertex(FaceTransform info, double[] buffer, int len, double[] newVert,
+                              double[] oldVert, double lenFirstOld, int oldSticker, boolean inverted)
+    {
+    for(int vertex=0; vertex<len; vertex++)
+      {
+      double newX = newVert[2*vertex  ];
+      double newY = newVert[2*vertex+1];
+      double lenIthNew = Math.sqrt(newX*newX + newY*newY);
+      double cos = QuatHelper.computeCos( oldVert[0], oldVert[1], newX, newY, lenIthNew, lenFirstOld);
+      double sin = QuatHelper.computeSin( oldVert[0], oldVert[1], newX, newY, lenIthNew, lenFirstOld);
+
+      rotateAllVertices(buffer,len,newVert,sin,cos);
+
+      if( isScaledVersionOf(buffer,oldVert,len,vertex,inverted) )
+        {
+        int newZeroIndex = computeRotatedIndex(0,len,vertex,inverted);
+        double scale = computeScale(oldVert,newVert,0,newZeroIndex);
+        correctInfo(info,scale,sin,cos,oldSticker,inverted);
+        return true;
+        }
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean successfullyCollapsedStickers(final FaceTransform newInfo, final FaceTransform oldInfo)
+    {
+    StickerCoords sNewInfo = mStickerCoords.get(newInfo.sticker);
+    StickerCoords sOldInfo = mStickerCoords.get(oldInfo.sticker);
+    double[] newVert = sNewInfo.vertices;
+    double[] oldVert = sOldInfo.vertices;
+    int oldLen = oldVert.length;
+    int newLen = newVert.length;
+
+    if( oldLen == newLen )
+      {
+      int oldSticker = oldInfo.sticker;
+      double[] buffer1 = new double[oldLen];
+      double lenFirstOld = Math.sqrt(oldVert[0]*oldVert[0] + oldVert[1]*oldVert[1]);
+      if( foundVertex(newInfo, buffer1, oldLen/2, newVert, oldVert, lenFirstOld, oldSticker, false) ) return true;
+      double[] buffer2 = new double[oldLen];
+      mirrorAllVertices(buffer2, newLen/2, newVert);
+      if( foundVertex(newInfo, buffer1, oldLen/2, buffer2, oldVert, lenFirstOld, oldSticker, true ) ) return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private double[][] constructVert(double[][] vertices, int[] index)
+    {
+    int len = index.length;
+    double[][] ret = new double[len][4];
+
+    for(int i=0; i<len; i++)
+      {
+      ret[i][0] = vertices[index[i]][0];
+      ret[i][1] = vertices[index[i]][1];
+      ret[i][2] = vertices[index[i]][2];
+      ret[i][3] = 1.0f;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void prepareAndRoundCorners(MeshBase mesh, double[][] vertices,
+                                      float[][] corners, int[] cornerIndexes,
+                                      float[][] centers, int[] centerIndexes )
+    {
+    int lenV = vertices.length;
+    Static3D[] staticVert = new Static3D[1];
+    Static3D center = new Static3D(0,0,0);
+
+    for(int v=0; v<lenV; v++)
+      {
+      staticVert[0] = new Static3D( (float)vertices[v][0], (float)vertices[v][1], (float)vertices[v][2]);
+
+      int cent = centerIndexes[v];
+
+      if( cent>=0 )
+        {
+        center.set( centers[cent][0], centers[cent][1], centers[cent][2]);
+
+        int corn = cornerIndexes[v];
+
+        if( corn>=0 )
+          {
+          float strength = corners[corn][0];
+          float radius   = corners[corn][1];
+          roundCorners(mesh, center, staticVert, strength, radius);
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void correctComponents(MeshBase mesh, int numComponents)
+    {
+    int numTexToBeAdded = numComponents-mesh.getNumTexComponents();
+
+    mesh.mergeEffComponents();
+
+    for(int i=0; i<numTexToBeAdded; i++ ) mesh.addEmptyTexComponent();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void printTransform(FaceTransform f)
+    {
+    android.util.Log.e("D", "q=("+f.qx+", "+f.qy+", "+f.qz+", "+f.qw+") v=("
+                       +f.vx+", "+f.vy+", "+f.vz+") scale="+f.scale+" sticker="+f.sticker);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC
+
+  public float[] computeBands(float H, int alpha, float dist, float K, int N)
+    {
+    float[] bands = new float[2*N];
+
+    bands[0] = 1.0f;
+    bands[1] = 0.0f;
+
+    float beta = (float)Math.atan(dist*Math.tan(Math.PI*alpha/180));
+    float sinBeta = (float)Math.sin(beta);
+    float cosBeta = (float)Math.cos(beta);
+    float R = cosBeta<1.0f ? H/(1.0f-cosBeta) : 0.0f;
+    float D = R*sinBeta;
+    float B = h(R,sinBeta,K*beta);
+
+    if( D>1.0f )
+      {
+      for(int i=1; i<N; i++)
+        {
+        bands[2*i  ] = (float)(N-1-i)/(N-1);
+        bands[2*i+1] = H*(1-bands[2*i]);
+        }
+      }
+    else
+      {
+      int K2 = (int)((N-3)*K);
+      int K1 = (N-3)-K2;
+
+      for(int i=0; i<=K1; i++)
+        {
+        float angle = K*beta + (1-K)*beta*(K1-i)/(K1+1);
+        float x = h(R,sinBeta,angle);
+        bands[2*i+2] = 1.0f - x;
+        bands[2*i+3] = g(R,D,x,cosBeta);
+        }
+
+      for(int i=0; i<=K2; i++)
+        {
+        float x = (1-B)*(i+1)/(K2+1) + B;
+        bands[2*K1+2 + 2*i+2] = 1.0f - x;
+        bands[2*K1+2 + 2*i+3] = g(R,D,f(D,B,x),cosBeta);
+        }
+      }
+
+    bands[2*N-2] = 0.0f;
+    bands[2*N-1] =    H;
+
+    return bands;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void roundCorners(MeshBase mesh, Static3D center, Static3D[] vertices, float strength, float regionRadius)
+    {
+    Static4D reg= new Static4D(0,0,0,regionRadius);
+
+    float centX = center.get0();
+    float centY = center.get1();
+    float centZ = center.get2();
+
+    for (Static3D vertex : vertices)
+      {
+      float x = strength*(centX - vertex.get0());
+      float y = strength*(centY - vertex.get1());
+      float z = strength*(centZ - vertex.get2());
+
+      VertexEffect effect = new VertexEffectDeform(new Static3D(x,y,z), RADIUS, vertex, reg);
+      mesh.apply(effect);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void printStickerCoords()
+    {
+    int stickers = mStickerCoords.size();
+
+    android.util.Log.d("D", "---- STICKER COORDS ----");
+
+    for(int s=0; s<stickers; s++)
+      {
+      String ver = "{ ";
+      StickerCoords info = mStickerCoords.get(s);
+      int len = info.vertices.length/2;
+
+      for(int i =0; i<len; i++)
+        {
+        if( i!=0 ) ver += ", ";
+        ver += ( (float)info.vertices[2*i]+"f, "+(float)info.vertices[2*i+1]+"f");
+        }
+
+      ver += " }";
+      android.util.Log.d("D", ver);
+      }
+
+    android.util.Log.d("D", "---- END STICKER COORDS ----");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void printFaceTransform()
+    {
+    android.util.Log.d("D", "---- OLD FACE TRANSFORM ---");
+
+    int oldfaces = mOldFaceTransf.size();
+
+    for(int f=0; f<oldfaces; f++)
+      {
+      printTransform(mOldFaceTransf.get(f));
+      }
+
+    android.util.Log.d("D", "---- NEW FACE TRANSFORM ---");
+
+    int newfaces = mNewFaceTransf.size();
+
+    for(int f=0; f<newfaces; f++)
+      {
+      printTransform(mNewFaceTransf.get(f));
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void clear()
+    {
+    mStickerCoords.clear();
+    mNewFaceTransf.clear();
+    mOldFaceTransf.clear();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void createNewFaceTransform( final double[][] vertices, final int[][] indexes)
+    {
+    FaceTransform ft;
+    int numNew = mNewFaceTransf.size();
+
+    for(int i=0; i<numNew; i++)
+      {
+      ft = mNewFaceTransf.remove(0);
+      mOldFaceTransf.add(ft);
+      }
+
+    int numFaces = indexes.length;
+    int numOld = mOldFaceTransf.size();
+
+    for (int face=0; face<numFaces; face++)
+      {
+      boolean collapsed = false;
+
+      double[][] vert = constructVert(vertices, indexes[face]);
+      FaceTransform newT = constructNewTransform(vert);
+
+      for (int old=0; !collapsed && old<numOld; old++)
+        {
+        ft = mOldFaceTransf.get(old);
+        if (successfullyCollapsedStickers(newT, ft)) collapsed = true;
+        }
+
+      for (int pre=0; !collapsed && pre<face; pre++)
+        {
+        ft = mNewFaceTransf.get(pre);
+        if (successfullyCollapsedStickers(newT, ft)) collapsed = true;
+        }
+
+      mNewFaceTransf.add(newT);
+      }
+    }
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void createNewFaceTransform(final ObjectShape shape)
+    {
+    double[][] vertices = shape.getVertices();
+    int[][] indices = shape.getVertIndices();
+    createNewFaceTransform(vertices,indices);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computeConvexityCenter(double[] out, float[] in, FaceTransform ft)
+    {
+    if( in==null )
+      {
+      out[0] = out[1] = 0.0f;
+      }
+    else
+      {
+      out[0] = in[0] - ft.vx;
+      out[1] = in[1] - ft.vy;
+      out[2] = in[2] - ft.vz;
+      out[3] = 1.0f;
+
+      mQuat1[0] =-ft.qx;
+      mQuat1[1] =-ft.qy;
+      mQuat1[2] =-ft.qz;
+      mQuat1[3] = ft.qw;
+
+      mQuat2[0] = -mQuat1[0];
+      mQuat2[1] = -mQuat1[1];
+      mQuat2[2] = -mQuat1[2];
+      mQuat2[3] = +mQuat1[3];
+
+      quatMultiply(mQuat1, out  , mQuat3);
+      quatMultiply(mQuat3, mQuat2, out  );
+
+      out[0] /= ft.scale;
+      out[1] /= ft.scale;
+      out[2] /= ft.scale;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public MeshBase createRoundedSolid(final ObjectShape shape)
+    {
+    double[][] vertices     = shape.getVertices();
+    int[][] vertIndexes     = shape.getVertIndices();
+    float[][] bands         = shape.getBands();
+    int[]   bandIndexes     = shape.getBandIndices();
+    float[][] corners       = shape.getCorners();
+    int[]   cornerIndexes   = shape.getCornerIndices();
+    float[][] centers       = shape.getCenters();
+    int[]   centerIndexes   = shape.getCenterIndices();
+    int numComponents       = shape.getNumComponents();
+    float[] convexityCenter = shape.getConvexityCenter();
+
+    return createRoundedSolid(vertices,vertIndexes,bands,bandIndexes,corners,cornerIndexes,
+                              centers,centerIndexes,numComponents,convexityCenter);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public MeshBase createRoundedSolid(final double[][] vertices, final int[][] vertIndexes,
+                                     final float[][] bands    , final int[]   bandIndexes,
+                                     final float[][] corners  , final int[]   cornerIndexes,
+                                     final float[][] centers  , final int[]   centerIndexes,
+                                     final int numComponents  , final float[] convexityCenter )
+    {
+    int numFaces = vertIndexes.length;
+    float[] band, bandsComputed;
+    MeshBase[] meshes = new MeshBase[numFaces];
+    FaceTransform fInfo;
+    StickerCoords sInfo;
+    double[] convexXY = new double[4];
+
+    for(int face=0; face<numFaces; face++)
+      {
+      fInfo = mNewFaceTransf.get(face);
+      sInfo = mStickerCoords.get(fInfo.sticker);
+
+      double[] verts = sInfo.vertices;
+      int lenVerts = verts.length;
+      float[] vertsFloat = new float[lenVerts];
+      for(int i=0; i<lenVerts; i++) vertsFloat[i] = (float)verts[i];
+
+      computeConvexityCenter(convexXY,convexityCenter,fInfo);
+
+      band = bands[bandIndexes[face]];
+      bandsComputed = computeBands( band[0], (int)band[1], band[2], band[3], (int)band[4]);
+      meshes[face] = new MeshPolygon(vertsFloat,bandsComputed,(int)band[5],(int)band[6], (float)convexXY[0], (float)convexXY[1]);
+      meshes[face].setEffectAssociation(0,(1<<face),0);
+      }
+
+    MeshBase mesh = new MeshJoined(meshes);
+    Static3D center = new Static3D(0,0,0);
+
+    for(int face=0; face<numFaces; face++)
+      {
+      int assoc = (1<<face);
+      fInfo = mNewFaceTransf.get(face);
+
+      float vx = (float)fInfo.vx;
+      float vy = (float)fInfo.vy;
+      float vz = (float)fInfo.vz;
+      float sc = (float)fInfo.scale;
+      float qx = (float)fInfo.qx;
+      float qy = (float)fInfo.qy;
+      float qz = (float)fInfo.qz;
+      float qw = (float)fInfo.qw;
+
+      Static3D scale = new Static3D(sc,sc, fInfo.flip ? -sc : sc);
+      Static3D move3D= new Static3D(vx,vy,vz);
+      Static4D quat  = new Static4D(qx,qy,qz,qw);
+
+      mesh.apply(new MatrixEffectScale(scale)           ,assoc,-1);
+      mesh.apply(new MatrixEffectQuaternion(quat,center),assoc,-1);
+      mesh.apply(new MatrixEffectMove(move3D)           ,assoc,-1);
+      }
+
+    prepareAndRoundCorners(mesh, vertices, corners, cornerIndexes, centers, centerIndexes);
+
+    correctComponents(mesh,numComponents);
+
+    return mesh;
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/FactorySticker.java b/src/main/java/org/distorted/objectlb/FactorySticker.java
new file mode 100644
index 00000000..de5837bc
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/FactorySticker.java
@@ -0,0 +1,315 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import static org.distorted.objectlb.TwistyObject.COLOR_BLACK;
+import static org.distorted.objectlb.TwistyObject.TEXTURE_HEIGHT;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class FactorySticker
+  {
+  private static FactorySticker mThis;
+  private float mOX, mOY, mR;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private FactorySticker()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static FactorySticker getInstance()
+    {
+    if( mThis==null ) mThis = new FactorySticker();
+
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float computeAngle(float dx, float dy)
+    {
+    float PI = (float)Math.PI;
+    double angle = Math.atan2(dy,dx);
+    float ret = (float)(3*PI/2-angle);
+
+    if( ret>2*PI ) ret-= 2*PI;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float getAngle(float[] angles, int index)
+    {
+    return angles==null ? 0 : angles[index];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computeCircleCoords(float lX,float lY, float rX, float rY, float alpha)
+    {
+    float ctg= 1.0f/((float)Math.tan(alpha));
+    mOX = 0.5f*(lX+rX) + ctg*0.5f*(lY-rY);
+    mOY = 0.5f*(lY+rY) - ctg*0.5f*(lX-rX);
+    float dx = mOX-lX;
+    float dy = mOY-lY;
+    mR = (float)Math.sqrt(dx*dx+dy*dy);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// circle1: center (x1,y1) radius r1; circle2: center (x2,y2) radius r2.
+// Guaranteed to intersect in two points. Find the intersection. Which one? the one that's closer
+// to (nearx,neary).
+
+  private void findCircleIntersection(float x1,float y1, float r1, float x2, float y2, float r2, float nearx, float neary )
+    {
+    float dx = x2-x1;
+    float dy = y2-y1;
+    float d = (float)Math.sqrt(dx*dx+dy*dy);
+
+    if( d>0 )
+      {
+      float Dx = dx/d;
+      float Dy = dy/d;
+      float cos = (r1*r1+d*d-r2*r2)/(2*r1*d);
+      float sin = (float)Math.sqrt(1-cos*cos);
+
+      float ox1 = x1 + r1*cos*Dx + r1*sin*Dy;
+      float oy1 = y1 + r1*cos*Dy - r1*sin*Dx;
+      float ox2 = x1 + r1*cos*Dx - r1*sin*Dy;
+      float oy2 = y1 + r1*cos*Dy + r1*sin*Dx;
+
+      dx = nearx-ox1;
+      dy = neary-oy1;
+      float d1 = dx*dx+dy*dy;
+      dx = nearx-ox2;
+      dy = neary-oy2;
+      float d2 = dx*dx+dy*dy;
+
+      if( d1<d2 )
+        {
+        mOX = ox1;
+        mOY = oy1;
+        }
+      else
+        {
+        mOX = ox2;
+        mOY = oy2;
+        }
+      }
+    else
+      {
+      mOX = x1;
+      mOY = y1;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void drawCurrCurveV(Canvas canvas, Paint paint, int left, int top, float r, float stroke, float pX, float pY, float cX, float cY, float nX, float nY, float pA, float cA)
+    {
+    pX = (0.5f+pX)*TEXTURE_HEIGHT;
+    pY = (0.5f-pY)*TEXTURE_HEIGHT;
+    cX = (0.5f+cX)*TEXTURE_HEIGHT;
+    cY = (0.5f-cY)*TEXTURE_HEIGHT;
+    nX = (0.5f+nX)*TEXTURE_HEIGHT;
+    nY = (0.5f-nY)*TEXTURE_HEIGHT;
+
+    computeCircleCoords(pX,pY,cX,cY,pA);
+    float o1x = mOX;
+    float o1y = mOY;
+    float r1  = mR;
+    computeCircleCoords(cX,cY,nX,nY,cA);
+    float o2x = mOX;
+    float o2y = mOY;
+    float r2  = mR;
+
+    float dx = o1x-pX;
+    float dy = o1y-pY;
+    float startA = computeAngle(dy,dx);
+    float sweepA = 2*pA;
+
+    startA *= 180/(Math.PI);
+    sweepA *= 180/(Math.PI);
+
+    canvas.drawArc( left+o1x-r1, top+o1y-r1, left+o1x+r1, top+o1y+r1, startA, sweepA, false, paint);
+
+    float r3 = r*TEXTURE_HEIGHT + stroke/2;
+    float R1 = r1 + (pA < 0 ? r3:-r3);
+    float R2 = r2 + (cA < 0 ? r3:-r3);
+    findCircleIntersection(o1x,o1y,R1,o2x,o2y,R2,cX,cY);
+    float o3x = mOX;
+    float o3y = mOY;
+
+    dx = pA<0 ? o3x-o1x : o1x-o3x;
+    dy = pA<0 ? o3y-o1y : o1y-o3y;
+    startA = computeAngle(dy,dx);
+    dx = cA<0 ? o3x-o2x : o2x-o3x;
+    dy = cA<0 ? o3y-o2y : o2y-o3y;
+    float endA = computeAngle(dy,dx);
+
+    sweepA = endA-startA;
+    if( sweepA<0 ) sweepA += 2*Math.PI;
+
+    startA *= 180/(Math.PI);
+    sweepA *= 180/(Math.PI);
+
+    canvas.drawArc( left+o3x-r3, top+o3y-r3, left+o3x+r3, top+o3y+r3, startA, sweepA, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void drawCurrVertex(Canvas canvas, Paint paint, int left, int top, float r, float stroke, float pX, float pY, float cX, float cY, float nX, float nY)
+    {
+    pX = (0.5f+pX)*TEXTURE_HEIGHT;
+    pY = (0.5f-pY)*TEXTURE_HEIGHT;
+    cX = (0.5f+cX)*TEXTURE_HEIGHT;
+    cY = (0.5f-cY)*TEXTURE_HEIGHT;
+    nX = (0.5f+nX)*TEXTURE_HEIGHT;
+    nY = (0.5f-nY)*TEXTURE_HEIGHT;
+
+    canvas.drawLine(left+pX,top+pY,left+cX,top+cY,paint);
+
+    float aX = pX-cX;
+    float aY = pY-cY;
+    float bX = cX-nX;
+    float bY = cY-nY;
+
+    float aLen = (float)Math.sqrt(aX*aX+aY*aY);
+    float bLen = (float)Math.sqrt(bX*bX+bY*bY);
+
+    aX /= aLen;
+    aY /= aLen;
+    bX /= bLen;
+    bY /= bLen;
+
+    float sX = (aX-bX)/2;
+    float sY = (aY-bY)/2;
+    float sLen = (float)Math.sqrt(sX*sX+sY*sY);
+    sX /= sLen;
+    sY /= sLen;
+
+    float startAngle = computeAngle(bX,-bY);
+    float endAngle   = computeAngle(aX,-aY);
+    float sweepAngle = endAngle-startAngle;
+    if( sweepAngle<0 ) sweepAngle += 2*Math.PI;
+
+    float R = r*TEXTURE_HEIGHT+stroke/2;
+    float C = (float)Math.cos(sweepAngle/2);
+    float A = R/C;
+
+    left += (cX+A*sX);
+    top  += (cY+A*sY);
+
+    if( C< (2*R-stroke)/(2*R+stroke) )
+      {
+      float alpha = startAngle + sweepAngle/2;
+      float B  = (R-stroke/2)/C;
+      float sx = (float)Math.cos(alpha);
+      float sy = (float)Math.sin(alpha);
+
+      float startX = left + R*sx;
+      float startY = top  + R*sy;
+      float stopX  = left + B*sx;
+      float stopY  = top  + B*sy;
+
+      canvas.drawLine(startX,startY,stopX,stopY,paint);
+      }
+
+    startAngle *= 180/(Math.PI);
+    sweepAngle *= 180/(Math.PI);
+
+    canvas.drawArc( left-R, top-R, left+R, top+R, startAngle, sweepAngle, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC
+
+  public void drawRoundedPolygon(Canvas canvas, Paint paint, int left, int top, int color, ObjectSticker sticker)
+    {
+    float stroke = sticker.getStroke();
+    float[] vertices = sticker.getCoords();
+    float[] angles = sticker.getCurvature();
+    float[] radii = sticker.getRadii();
+
+    stroke *= TEXTURE_HEIGHT;
+
+    paint.setAntiAlias(true);
+    paint.setStrokeWidth(stroke);
+    paint.setColor(color);
+    paint.setStyle(Paint.Style.FILL);
+
+    canvas.drawRect(left,top,left+TEXTURE_HEIGHT,top+TEXTURE_HEIGHT,paint);
+
+    paint.setColor(COLOR_BLACK);
+    paint.setStyle(Paint.Style.STROKE);
+
+    int length = vertices.length;
+    int numVertices = length/2;
+
+    float prevX = vertices[length-2];
+    float prevY = vertices[length-1];
+    float currX = vertices[0];
+    float currY = vertices[1];
+    float nextX = vertices[2];
+    float nextY = vertices[3];
+
+    float prevA = getAngle(angles,numVertices-1);
+    float currA = getAngle(angles,0);
+
+    for(int vert=0; vert<numVertices; vert++)
+      {
+      if( prevA==0 )
+        {
+        drawCurrVertex(canvas, paint, left, top, radii[vert], stroke, prevX,prevY,currX,currY,nextX,nextY);
+        }
+      else
+        {
+        drawCurrCurveV(canvas, paint, left, top, radii[vert], stroke, prevX,prevY,currX,currY,nextX,nextY,prevA,currA);
+        }
+
+      prevX = currX;
+      prevY = currY;
+      currX = nextX;
+      currY = nextY;
+
+      prevA = currA;
+      currA = getAngle(angles, vert==numVertices-1 ? 0 : vert+1);
+
+      if( 2*(vert+2)+1 < length )
+        {
+        nextX = vertices[2*(vert+2)  ];
+        nextY = vertices[2*(vert+2)+1];
+        }
+      else
+        {
+        nextX = vertices[0];
+        nextY = vertices[1];
+        }
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/Movement.java b/src/main/java/org/distorted/objectlb/Movement.java
new file mode 100644
index 00000000..546b7042
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Movement.java
@@ -0,0 +1,477 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class Movement
+  {
+  // it doesn't matter where we touch a face - the list of enabled rotAxis will always be the same
+  public static final int TYPE_NOT_SPLIT    = 0;
+  // each face is split into several parts by lines coming from its center to the midpoints of each edge
+  public static final int TYPE_SPLIT_EDGE   = 1;
+  // each face is split into several parts by lines coming from its center to the vertices
+  public static final int TYPE_SPLIT_CORNER = 2;
+
+  static final float SQ3 = (float)Math.sqrt(3);
+  static final float SQ6 = (float)Math.sqrt(6);
+
+  private final int mNumFaceAxis;
+  private final float[] mPoint, mCamera, mTouch;
+  private final float[] mPoint2D, mMove2D;
+  private final int[] mEnabledRotAxis;
+  private final float mDistanceCenterFace3D;
+  private final Static3D[] mFaceAxis;
+
+  private int mLastTouchedFace;
+  private float[][][] mCastedRotAxis;
+  private Static4D[][] mCastedRotAxis4D;
+  private float[][] mTouchBorders, mA, mB;
+
+  private final int mType;
+  private final int[][][] mEnabled;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract int returnPart(int type, int face, float[] touchPoint);
+  abstract boolean isInsideFace(int face, float[] point);
+  public abstract float returnRotationFactor(int numLayers, int row);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Movement(Static3D[] rotAxis, Static3D[] faceAxis, float[][] cuts, boolean[][] rotatable,
+           float distance3D, int size, int type, int[][][] enabled)
+    {
+    mPoint = new float[3];
+    mCamera= new float[3];
+    mTouch = new float[3];
+
+    mPoint2D = new float[2];
+    mMove2D  = new float[2];
+
+    mType = type;
+    mEnabled = enabled;
+
+    mFaceAxis   = faceAxis;
+    mNumFaceAxis= mFaceAxis.length;
+
+    mEnabledRotAxis = new int[rotAxis.length+1];
+
+    mDistanceCenterFace3D = distance3D; // distance from the center of the object to each of its faces
+
+    computeCastedAxis(rotAxis);
+    computeBorders(cuts,rotatable,size);
+    computeLinear(distance3D,rotAxis,faceAxis);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// mCastedRotAxis[1][2]{0,1} are the 2D coords of the 2nd rotAxis cast onto the face defined by the
+// 1st faceAxis.
+
+  private void computeCastedAxis(Static3D[] rotAxis)
+    {
+    mCastedRotAxis   = new float[mNumFaceAxis][rotAxis.length][2];
+    mCastedRotAxis4D = new Static4D[mNumFaceAxis][rotAxis.length];
+
+    float fx,fy,fz,f;
+
+    for( int casted=0; casted<rotAxis.length; casted++)
+      {
+      Static3D a = rotAxis[casted];
+      mPoint[0]= a.get0();
+      mPoint[1]= a.get1();
+      mPoint[2]= a.get2();
+
+      for( int face=0; face<mNumFaceAxis; face++)
+        {
+        convertTo2Dcoords( mPoint, mFaceAxis[face], mCastedRotAxis[face][casted]);
+        normalize2D(mCastedRotAxis[face][casted]);
+
+        fx = mFaceAxis[face].get0();
+        fy = mFaceAxis[face].get1();
+        fz = mFaceAxis[face].get2();
+        f  = mPoint[0]*fx + mPoint[1]*fy + mPoint[2]*fz;
+        mCastedRotAxis4D[face][casted] = new Static4D( mPoint[0]-f*fx, mPoint[1]-f*fy, mPoint[2]-f*fz, 0);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void normalize2D(float[] vect)
+    {
+    float len = (float)Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]);
+    vect[0] /= len;
+    vect[1] /= len;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// find the casted axis with which our move2D vector forms an angle closest to 90 deg.
+
+  private int computeRotationIndex(int faceAxis, float[] move2D, int[] enabled)
+    {
+    float cosAngle, minCosAngle = Float.MAX_VALUE;
+    int minIndex=0, index;
+    float m0 = move2D[0];
+    float m1 = move2D[1];
+    float len = (float)Math.sqrt(m0*m0 + m1*m1);
+
+    if( len!=0.0f )
+      {
+      m0 /= len;
+      m1 /= len;
+      }
+    else
+      {
+      m0 = 1.0f;  // arbitrarily
+      m1 = 0.0f;  //
+      }
+
+    int numAxis = enabled[0];
+
+    for(int axis=1; axis<=numAxis; axis++)
+      {
+      index = enabled[axis];
+      cosAngle = m0*mCastedRotAxis[faceAxis][index][0] + m1*mCastedRotAxis[faceAxis][index][1];
+      if( cosAngle<0 ) cosAngle = -cosAngle;
+
+      if( cosAngle<minCosAngle )
+        {
+        minCosAngle=cosAngle;
+        minIndex = index;
+        }
+      }
+
+    return minIndex;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// in the center of the face offset is always 0 regardless of the axis
+
+  private float computeOffset(float[] point, float[] axis)
+    {
+    return point[0]*axis[0] + point[1]*axis[1];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean faceIsVisible(Static3D faceAxis)
+    {
+    float castCameraOnAxis = mCamera[0]*faceAxis.get0() + mCamera[1]*faceAxis.get1() + mCamera[2]*faceAxis.get2();
+    return castCameraOnAxis > mDistanceCenterFace3D;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// given precomputed mCamera and mPoint, respectively camera and touch point positions in ScreenSpace,
+// compute point 'output[]' which:
+// 1) lies on a face of the Object, i.e. surface defined by (axis, distance from (0,0,0))
+// 2) is co-linear with mCamera and mPoint
+//
+// output = camera + alpha*(point-camera), where alpha = [dist-axis*camera] / [axis*(point-camera)]
+
+  private void castTouchPointOntoFace(Static3D faceAxis, float[] output)
+    {
+    float d0 = mPoint[0]-mCamera[0];
+    float d1 = mPoint[1]-mCamera[1];
+    float d2 = mPoint[2]-mCamera[2];
+    float a0 = faceAxis.get0();
+    float a1 = faceAxis.get1();
+    float a2 = faceAxis.get2();
+
+    float denom = a0*d0 + a1*d1 + a2*d2;
+
+    if( denom != 0.0f )
+      {
+      float axisCam = a0*mCamera[0] + a1*mCamera[1] + a2*mCamera[2];
+      float alpha = (mDistanceCenterFace3D-axisCam)/denom;
+
+      output[0] = mCamera[0] + d0*alpha;
+      output[1] = mCamera[1] + d1*alpha;
+      output[2] = mCamera[2] + d2*alpha;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Convert the 3D point3D into a 2D point on the same face surface, but in a different
+// coordinate system: a in-plane 2D coord where the origin is in the point where the axis intersects
+// the surface, and whose Y axis points 'north' i.e. is in the plane given by the 3D origin, the
+// original 3D Y axis and our 2D in-plane origin.
+// If those 3 points constitute a degenerate triangle which does not define a plane - which can only
+// happen if axis is vertical (or in theory when 2D origin and 3D origin meet, but that would have to
+// mean that the distance between the center of the Object and its faces is 0) - then we arbitrarily
+// decide that 2D Y = (0,0,-1) in the North Pole and (0,0,1) in the South Pole)
+
+  private void convertTo2Dcoords(float[] point3D, Static3D faceAxis, float[] output)
+    {
+    float y0,y1,y2; // base Y vector of the 2D coord system
+    float a0 = faceAxis.get0();
+    float a1 = faceAxis.get1();
+    float a2 = faceAxis.get2();
+
+    if( a0==0.0f && a2==0.0f )
+      {
+      y0=0; y1=0; y2=-a1;
+      }
+    else if( a1==0.0f )
+      {
+      y0=0; y1=1; y2=0;
+      }
+    else
+      {
+      float norm = (float)(-a1/Math.sqrt(1-a1*a1));
+      y0 = norm*a0; y1= norm*(a1-1/a1); y2=norm*a2;
+      }
+
+    float x0 = y1*a2 - y2*a1;  //
+    float x1 = y2*a0 - y0*a2;  // (2D coord baseY) x (axis) = 2D coord baseX
+    float x2 = y0*a1 - y1*a0;  //
+
+    float originAlpha = point3D[0]*a0 + point3D[1]*a1 + point3D[2]*a2;
+
+    float origin0 = originAlpha*a0; // coords of the point where axis
+    float origin1 = originAlpha*a1; // intersects surface plane i.e.
+    float origin2 = originAlpha*a2; // the origin of our 2D coord system
+
+    float v0 = point3D[0] - origin0;
+    float v1 = point3D[1] - origin1;
+    float v2 = point3D[2] - origin2;
+
+    output[0] = v0*x0 + v1*x1 + v2*x2;
+    output[1] = v0*y0 + v1*y1 + v2*y2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float[] computeBorder(float[] cuts, boolean[] rotatable, int size)
+    {
+    int len = cuts.length;
+    float[] border = new float[len];
+
+    for(int i=0; i<len; i++)
+      {
+      if( !rotatable[i] )
+        {
+        border[i] = i>0 ? border[i-1] : -Float.MAX_VALUE;
+        }
+      else
+        {
+        if( rotatable[i+1] ) border[i] = cuts[i]/size;
+        else
+          {
+          int found = -1;
+
+          for(int j=i+2; j<=len; j++)
+            {
+            if( rotatable[j] )
+              {
+              found=j;
+              break;
+              }
+            }
+
+          border[i] = found>0 ? (cuts[i]+cuts[found-1])/(2*size) : Float.MAX_VALUE;
+          }
+        }
+      }
+
+    return border;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// size, not numLayers (see Master Skewb where size!=numLayers)
+
+  void computeBorders(float[][] cuts, boolean[][] rotatable, int size)
+    {
+    int numCuts = cuts.length;
+    mTouchBorders = new float[numCuts][];
+
+    for(int i=0; i<numCuts; i++)
+      {
+      mTouchBorders[i] = computeBorder(cuts[i],rotatable[i],size);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeSign(Static3D a, Static3D b)
+    {
+    float a1 = a.get0();
+    float a2 = a.get1();
+    float a3 = a.get2();
+    float b1 = b.get0();
+    float b2 = b.get1();
+    float b3 = b.get2();
+
+    return a1*b1+a2*b2+a3*b3 < 0 ? 1:-1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float crossProductLen(Static3D a, Static3D b)
+    {
+    float a1 = a.get0();
+    float a2 = a.get1();
+    float a3 = a.get2();
+    float b1 = b.get0();
+    float b2 = b.get1();
+    float b3 = b.get2();
+
+    float x1 = a2*b3-a3*b2;
+    float x2 = a3*b1-a1*b3;
+    float x3 = a1*b2-a2*b1;
+
+    return (float)Math.sqrt(x1*x1 + x2*x2 + x3*x3);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// compute the array of 'A' and 'B' coeffs of the Ax+B linear function by which we need to multiply
+// the 3D 'cuts' to translate it from 3D (i.e. with respect to the rotAxis) to 2D in-face (i.e. with
+// respect to the 2D rotAxis cast into a particular face)
+
+  private void computeLinear(float distance3D, Static3D[] rotAxis, Static3D[] faceAxis)
+    {
+    int numFaces = faceAxis.length;
+    int numRot   = rotAxis.length;
+
+    mA = new float[numFaces][numRot];
+    mB = new float[numFaces][numRot];
+
+    for(int i=0; i<numFaces; i++)
+      for(int j=0; j<numRot; j++)
+        {
+        mA[i][j] = crossProductLen(faceAxis[i],rotAxis[j]);
+
+        if( mA[i][j]!=0.0f )
+          {
+          float coeff = (float)Math.sqrt(1/(mA[i][j]*mA[i][j]) -1);
+          int sign = computeSign(faceAxis[i],rotAxis[j]);
+          mB[i][j] = sign*distance3D*coeff;
+          }
+        else mB[i][j] = 0.0f;
+        }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeRowFromOffset(int face, int axisIndex, float offset)
+    {
+    float[] borders = mTouchBorders[axisIndex];
+    int len = borders.length;
+    float A = mA[face][axisIndex];
+
+    if( A!=0.0f )
+      {
+      float B = mB[face][axisIndex];
+
+      for(int i=0; i<len; i++)
+        {
+        float translated = B + borders[i]/A;
+        if( offset<translated ) return i;
+        }
+      }
+
+    return len;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void computeEnabledAxis(int face, float[] touchPoint, int[] enabled)
+    {
+    int part = returnPart(mType,face,touchPoint);
+
+    int num = mEnabled[face][0].length;
+    enabled[0] = num;
+    System.arraycopy(mEnabled[face][part], 0, enabled, 1, num);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean faceTouched(Static4D rotatedTouchPoint, Static4D rotatedCamera, float objectRatio)
+    {
+    mPoint[0]  = rotatedTouchPoint.get0()/objectRatio;
+    mPoint[1]  = rotatedTouchPoint.get1()/objectRatio;
+    mPoint[2]  = rotatedTouchPoint.get2()/objectRatio;
+
+    mCamera[0] = rotatedCamera.get0()/objectRatio;
+    mCamera[1] = rotatedCamera.get1()/objectRatio;
+    mCamera[2] = rotatedCamera.get2()/objectRatio;
+
+    for( mLastTouchedFace=0; mLastTouchedFace<mNumFaceAxis; mLastTouchedFace++)
+      {
+      if( faceIsVisible(mFaceAxis[mLastTouchedFace]) )
+        {
+        castTouchPointOntoFace(mFaceAxis[mLastTouchedFace], mTouch);
+        convertTo2Dcoords(mTouch, mFaceAxis[mLastTouchedFace], mPoint2D);
+        if( isInsideFace(mLastTouchedFace,mPoint2D) ) return true;
+        }
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static2D newRotation(Static4D rotatedTouchPoint, float objectRatio)
+    {
+    mPoint[0] = rotatedTouchPoint.get0()/objectRatio;
+    mPoint[1] = rotatedTouchPoint.get1()/objectRatio;
+    mPoint[2] = rotatedTouchPoint.get2()/objectRatio;
+
+    castTouchPointOntoFace(mFaceAxis[mLastTouchedFace], mTouch);
+    convertTo2Dcoords(mTouch, mFaceAxis[mLastTouchedFace], mMove2D);
+
+    mMove2D[0] -= mPoint2D[0];
+    mMove2D[1] -= mPoint2D[1];
+
+    computeEnabledAxis(mLastTouchedFace, mPoint2D, mEnabledRotAxis);
+    int rotIndex = computeRotationIndex(mLastTouchedFace, mMove2D, mEnabledRotAxis);
+    float offset = computeOffset(mPoint2D, mCastedRotAxis[mLastTouchedFace][rotIndex]);
+    int row      = computeRowFromOffset(mLastTouchedFace,rotIndex,offset);
+
+    return new Static2D(rotIndex,row);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static4D getCastedRotAxis(int rotIndex)
+    {
+    return mCastedRotAxis4D[mLastTouchedFace][rotIndex];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getTouchedFace()
+    {
+    return mLastTouchedFace;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getTouchedPoint3D()
+    {
+    return mTouch;
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/Movement12.java b/src/main/java/org/distorted/objectlb/Movement12.java
new file mode 100644
index 00000000..2bded588
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Movement12.java
@@ -0,0 +1,187 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import static org.distorted.objectlb.TwistyObject.SQ5;
+
+import org.distorted.library.type.Static3D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Dodecahedral objects: map the 2D swipes of user's fingers to 3D rotations
+
+public class Movement12 extends Movement
+{
+  public static final float C2       = (SQ5+3)/4;
+  public static final float LEN      = (float)(Math.sqrt(1.25f+0.5f*SQ5));
+  public static final float SIN54    = (SQ5+1)/4;
+  public static final float COS54    = (float)(Math.sqrt(10-2*SQ5)/4);
+
+  public static final float DIST3D = (float)Math.sqrt(0.625f+0.275f*SQ5);
+  public static final float DIST2D = (SIN54/COS54)/2;
+
+  public static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(    C2/LEN, SIN54/LEN,    0      ),
+           new Static3D(    C2/LEN,-SIN54/LEN,    0      ),
+           new Static3D(   -C2/LEN, SIN54/LEN,    0      ),
+           new Static3D(   -C2/LEN,-SIN54/LEN,    0      ),
+           new Static3D( 0        ,    C2/LEN, SIN54/LEN ),
+           new Static3D( 0        ,    C2/LEN,-SIN54/LEN ),
+           new Static3D( 0        ,   -C2/LEN, SIN54/LEN ),
+           new Static3D( 0        ,   -C2/LEN,-SIN54/LEN ),
+           new Static3D( SIN54/LEN,    0     ,    C2/LEN ),
+           new Static3D( SIN54/LEN,    0     ,   -C2/LEN ),
+           new Static3D(-SIN54/LEN,    0     ,    C2/LEN ),
+           new Static3D(-SIN54/LEN,    0     ,   -C2/LEN )
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Movement12(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
+    {
+    super(rotAxis, FACE_AXIS, cuts,rotatable,DIST3D, size, type, enabled);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(int numLayers, int row)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return angle (in radians) that the line connecting the center C of the pentagonal face and the
+// first vertex of the pentagon makes with a vertical line coming upwards from the center C.
+
+  private float returnAngle(int face)
+    {
+    switch(face)
+      {
+      case  0:
+      case  2:
+      case  6:
+      case  7: return 0.0f;
+      case  1:
+      case  3:
+      case  4:
+      case  5: return (float)(36*Math.PI/180);
+      case  9:
+      case 10: return (float)(54*Math.PI/180);
+      case  8:
+      case 11: return (float)(18*Math.PI/180);
+      }
+
+    return 0.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// The pair (distance,angle) defines a point P in R^2 in polar coordinate system. Let V be the vector
+// from the center of the coordinate system to P.
+// Let P' be the point defined by polar (distance,angle+PI/2). Let Lh be the half-line starting at
+// P' and going in the direction of V.
+// Return true iff point 'point' lies on the left of Lh, i.e. when we rotate (using the center of
+// the coordinate system as the center of rotation) 'point' and Lh in such a way that Lh points
+// directly upwards, is 'point' on the left or the right of it?
+
+  private boolean isOnTheLeft(float[] point, float distance, float angle)
+    {
+    float sin = (float)Math.sin(angle);
+    float cos = (float)Math.cos(angle);
+
+    float vx = point[0] + sin*distance;
+    float vy = point[1] - cos*distance;
+
+    return vx*sin < vy*cos;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int returnPart(int type, int face, float[] point)
+    {
+    switch(type)
+      {
+      case TYPE_SPLIT_EDGE  : return partEdge(point,face);
+      case TYPE_SPLIT_CORNER: return partCorner(point,face);
+      default               : return 0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Return 0,1,2,3,4 - the vertex of the pentagon to which point 'point' is the closest, if the
+// 'point' is inside the pentagon - or -1 otherwise.
+// The 'first' vertex is the one we meet the first when we rotate clockwise starting from 12:00.
+// This vertex makes angle 'returnAngle()' with the line coming out upwards from the center of the
+// pentagon.
+// Distance from the center to a vertex of the pentagon = 1/(6*COS54)
+
+  int partEdge(float[] point, int face)
+    {
+    float angle = returnAngle(face);
+    float A = (float)(Math.PI/5);
+
+    for(int i=0; i<5; i++)
+      {
+      if( isOnTheLeft(point, DIST2D, (9-2*i)*A-angle) ) return -1;
+      }
+
+    if( isOnTheLeft(point, 0, 2.5f*A-angle) )
+      {
+      if( isOnTheLeft(point, 0, 3.5f*A-angle) )
+        {
+        return isOnTheLeft(point, 0, 5.5f*A-angle) ? 3 : 4;
+        }
+      else return 0;
+      }
+    else
+      {
+      if( isOnTheLeft(point, 0, 4.5f*A-angle) )
+        {
+        return 2;
+        }
+      else
+        {
+        return isOnTheLeft(point, 0, 6.5f*A-angle) ? 1 : 0;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// TODO - no such object yet
+
+  int partCorner(float[] point, int face)
+    {
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean isInsideFace(int face, float[] p)
+    {
+    float angle = returnAngle(face);
+    float A = (float)(Math.PI/5);
+
+    for(int i=0; i<5; i++)
+      {
+      if( isOnTheLeft(p, DIST2D, (9-2*i)*A-angle) ) return false;
+      }
+
+    return true;
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Movement4.java b/src/main/java/org/distorted/objectlb/Movement4.java
new file mode 100644
index 00000000..06f07016
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Movement4.java
@@ -0,0 +1,102 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import org.distorted.library.type.Static3D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Tetrahedral objects: map the 2D swipes of user's fingers to 3D rotations
+
+public class Movement4 extends Movement
+{
+  public static final float DIST3D = SQ6/12;
+  public static final float DIST2D = SQ3/6;
+
+  public static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(     0,+SQ3/3,+SQ6/3),
+           new Static3D(     0,+SQ3/3,-SQ6/3),
+           new Static3D(-SQ6/3,-SQ3/3,     0),
+           new Static3D(+SQ6/3,-SQ3/3,     0),
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Movement4(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
+    {
+    super(rotAxis, FACE_AXIS, cuts, rotatable, DIST3D, size, type, enabled);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// corner    edge
+//   |       \ 0 /
+// 2 | 0      \ /
+//  / \      2 | 1
+// / 1 \       |
+
+  int returnPart(int type, int face, float[] touchPoint)
+    {
+    switch(type)
+      {
+      case TYPE_NOT_SPLIT   : return 0;
+
+      case TYPE_SPLIT_EDGE  : float y1 = (face > 1 ? touchPoint[1] : -touchPoint[1]);
+                              float x1 = touchPoint[0];
+
+                              boolean e0 = x1>0;
+                              boolean e1 = y1>(+SQ3/3)*x1;
+                              boolean e2 = y1>(-SQ3/3)*x1;
+
+                              if(  e1 && e2 ) return 0;
+                              if( !e1 && e0 ) return 1;
+                              if( !e0 &&!e2 ) return 2;
+
+      case TYPE_SPLIT_CORNER: float y2 = (face > 1 ? touchPoint[1] : -touchPoint[1]);
+                              float x2 = touchPoint[0];
+
+                              boolean c0 = x2>0;
+                              boolean c1 = y2>(+SQ3/3)*x2;
+                              boolean c2 = y2>(-SQ3/3)*x2;
+
+                              if(  c0 && c2 ) return 0;
+                              if( !c1 &&!c2 ) return 1;
+                              if( !c0 && c1 ) return 2;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Jing has nL=2
+
+  public float returnRotationFactor(int numLayers, int row)
+    {
+    return numLayers==2 ? 1.0f : ((float)numLayers)/(numLayers-row);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean isInsideFace(int face, float[] p)
+    {
+    float y = (face > 1 ? p[1] : -p[1]);
+    float x = p[0];
+    return (y >= -DIST2D) && (y <= DIST2D*(2-6*x)) && (y <= DIST2D*(2+6*x));
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Movement6.java b/src/main/java/org/distorted/objectlb/Movement6.java
new file mode 100644
index 00000000..f3484a5c
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Movement6.java
@@ -0,0 +1,82 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import org.distorted.library.type.Static3D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Hexahedral objects: map the 2D swipes of user's fingers to 3D rotations
+
+public class Movement6 extends Movement
+{
+  public static final float DIST3D = 0.5f;
+  public static final float DIST2D = 0.5f;
+
+  public 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)
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Movement6(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
+    {
+    super(rotAxis, FACE_AXIS, cuts, rotatable, DIST3D, size, type, enabled);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+//  corner    edge
+//  \ 0 /     3 | 0
+// 3 \ / 1  ___ | ___
+//   / \        |
+//  / 2 \     2 | 1
+
+  int returnPart(int type, int face, float[] touchPoint)
+    {
+    switch(type)
+      {
+      case TYPE_NOT_SPLIT   : return 0;
+      case TYPE_SPLIT_EDGE  : boolean e0 = touchPoint[0] > 0;
+                              boolean e1 = touchPoint[1] > 0;
+                              return e0 ? (e1 ? 0:1) : (e1 ? 3:2);
+      case TYPE_SPLIT_CORNER: boolean c0 = touchPoint[1] >= touchPoint[0];
+                              boolean c1 = touchPoint[1] >=-touchPoint[0];
+                              return c0 ? (c1 ? 0:3) : (c1 ? 1:2);
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(int numLayers, int row)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean isInsideFace(int face, float[] p)
+    {
+    return ( p[0]<=DIST2D && p[0]>=-DIST2D && p[1]<=DIST2D && p[1]>=-DIST2D );
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Movement8.java b/src/main/java/org/distorted/objectlb/Movement8.java
new file mode 100644
index 00000000..6a5ac37e
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Movement8.java
@@ -0,0 +1,101 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import org.distorted.library.type.Static3D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Octahedral objects: map the 2D swipes of user's fingers to 3D rotations
+
+public class Movement8 extends Movement
+{
+  public static final float DIST3D = SQ6/6;
+  public static final float DIST2D = SQ3/6;
+
+  public 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)
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Movement8(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
+    {
+    super(rotAxis, FACE_AXIS, cuts, rotatable, DIST3D, size, type, enabled);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// corner    edge
+//   |       \ 0 /
+// 2 | 0      \ /
+//  / \      2 | 1
+// / 1 \       |
+
+  int returnPart(int type, int face, float[] touchPoint)
+    {
+    switch(type)
+      {
+      case TYPE_NOT_SPLIT   : return 0;
+
+      case TYPE_SPLIT_EDGE  : float y1 = (face%2 == 0 ? touchPoint[1] : -touchPoint[1]);
+                              float x1 = touchPoint[0];
+
+                              boolean e0 = x1>0;
+                              boolean e1 = y1>(+SQ3/3)*x1;
+                              boolean e2 = y1>(-SQ3/3)*x1;
+
+                              if(  e1 && e2 ) return 0;
+                              if( !e1 && e0 ) return 1;
+                              if( !e0 &&!e2 ) return 2;
+
+      case TYPE_SPLIT_CORNER: float y2 = (face%2 == 0 ? touchPoint[1] : -touchPoint[1]);
+                              float x2 = touchPoint[0];
+
+                              boolean c0 = x2>0;
+                              boolean c1 = y2>(+SQ3/3)*x2;
+                              boolean c2 = y2>(-SQ3/3)*x2;
+
+                              if(  c0 && c2 ) return 0;
+                              if( !c1 &&!c2 ) return 1;
+                              if( !c0 && c1 ) return 2;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(int numLayers, int row)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean isInsideFace(int face, float[] p)
+    {
+    float y = (face%2 == 0 ? p[1] : -p[1]);
+    float x = p[0];
+    return (y >= -DIST2D) && (y <= DIST2D*(2-6*x)) && (y <= DIST2D*(2+6*x));
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/ObjectList.java b/src/main/java/org/distorted/objectlb/ObjectList.java
new file mode 100644
index 00000000..b14f5503
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/ObjectList.java
@@ -0,0 +1,587 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+import android.content.res.Resources;
+
+import org.distorted.objects.TwistyBandaged2Bar;
+import org.distorted.objects.TwistyBandaged3Plate;
+import org.distorted.objects.TwistyBandagedEvil;
+import org.distorted.objects.TwistyBandagedFused;
+import org.distorted.objects.TwistyCube;
+import org.distorted.objects.TwistyDiamond;
+import org.distorted.objects.TwistyDino4;
+import org.distorted.objects.TwistyDino6;
+import org.distorted.objects.TwistyHelicopter;
+import org.distorted.objects.TwistyIvy;
+import org.distorted.objects.TwistyJing;
+import org.distorted.objects.TwistyKilominx;
+import org.distorted.objects.TwistyMegaminx;
+import org.distorted.objects.TwistyMirror;
+import org.distorted.objects.TwistyPyraminx;
+import org.distorted.objects.TwistyRedi;
+import org.distorted.objects.TwistyRex;
+import org.distorted.objects.TwistySkewb;
+import org.distorted.objects.TwistySquare1;
+import org.distorted.objects.TwistySquare2;
+import org.distorted.objects.TwistyUltimate;
+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;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum ObjectList
+  {
+  ///////////////////// Size // DB Level // NumScrambles // Mesh // small icon // medium icon // big icon // huge icon
+
+  CUBE (
+         new int[][] {
+                       {2 , 12, 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, 17, 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, 24, 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, 28, R.raw.cube5, R.drawable.ui_small_cube5, R.drawable.ui_medium_cube5, R.drawable.ui_big_cube5, R.drawable.ui_huge_cube5}
+                     },
+         0
+       ),
+
+  JING (
+         new int[][] {
+                       {2 , 11, 11, R.raw.jing, R.drawable.ui_small_jing2, R.drawable.ui_medium_jing2, R.drawable.ui_big_jing2, R.drawable.ui_huge_jing2} ,
+                     },
+         1
+       ),
+
+  PYRA (
+         new int[][] {
+                       {3 , 10, 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, 17, 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, 23, R.raw.pyra5, R.drawable.ui_small_pyra5, R.drawable.ui_medium_pyra5, R.drawable.ui_big_pyra5, R.drawable.ui_huge_pyra5}
+                     },
+         1
+       ),
+
+  KILO (
+         new int[][] {
+                       {3 , 18, 18, R.raw.kilo3, R.drawable.ui_small_kilo3, R.drawable.ui_medium_kilo3, R.drawable.ui_big_kilo3, R.drawable.ui_huge_kilo3} ,
+                       {5 , 33, 33, R.raw.kilo5, R.drawable.ui_small_kilo5, R.drawable.ui_medium_kilo5, R.drawable.ui_big_kilo5, R.drawable.ui_huge_kilo5} ,
+                     },
+         2
+       ),
+
+  MEGA (
+         new int[][] {
+                       {3 , 21, 21, R.raw.mega3, R.drawable.ui_small_mega3, R.drawable.ui_medium_mega3, R.drawable.ui_big_mega3, R.drawable.ui_huge_mega3} ,
+                       {5 , 35, 37, R.raw.mega5, R.drawable.ui_small_mega5, R.drawable.ui_medium_mega5, R.drawable.ui_big_mega5, R.drawable.ui_huge_mega5} ,
+                     },
+         2
+       ),
+
+  ULTI (
+         new int[][] {
+                       {2 , 18, 18, R.raw.ulti, R.drawable.ui_small_ulti, R.drawable.ui_medium_ulti, R.drawable.ui_big_ulti, R.drawable.ui_huge_ulti} ,
+                     },
+         3
+       ),
+
+  DIAM (
+         new int[][] {
+                       {2 , 10, 12, R.raw.diam2, R.drawable.ui_small_diam2, R.drawable.ui_medium_diam2, R.drawable.ui_big_diam2, R.drawable.ui_huge_diam2} ,
+                       {3 , 18, 24, R.raw.diam3, R.drawable.ui_small_diam3, R.drawable.ui_medium_diam3, R.drawable.ui_big_diam3, R.drawable.ui_huge_diam3} ,
+                       {4 , 32, 32, R.raw.diam4, R.drawable.ui_small_diam4, R.drawable.ui_medium_diam4, R.drawable.ui_big_diam4, R.drawable.ui_huge_diam4} ,
+                     },
+         3
+       ),
+
+  DINO (
+         new int[][] {
+                       {3 , 10, 10, R.raw.dino, R.drawable.ui_small_dino, R.drawable.ui_medium_dino, R.drawable.ui_big_dino, R.drawable.ui_huge_dino} ,
+                     },
+         4
+       ),
+
+  DIN4 (
+         new int[][] {
+                       {3 , 7, 7, R.raw.dino, R.drawable.ui_small_din4, R.drawable.ui_medium_din4, R.drawable.ui_big_din4, R.drawable.ui_huge_din4} ,
+                     },
+         4
+       ),
+
+  REDI (
+         new int[][] {
+                       {3 , 14, 16, R.raw.redi, R.drawable.ui_small_redi, R.drawable.ui_medium_redi, R.drawable.ui_big_redi, R.drawable.ui_huge_redi} ,
+                     },
+         4
+       ),
+
+  HELI (
+         new int[][] {
+                       {3 , 18, 20, R.raw.heli, R.drawable.ui_small_heli, R.drawable.ui_medium_heli, R.drawable.ui_big_heli, R.drawable.ui_huge_heli} ,
+                     },
+         4
+       ),
+
+  SKEW (
+         new int[][] {
+                       {2 , 11, 11, R.raw.skew2, R.drawable.ui_small_skewb, R.drawable.ui_medium_skewb, R.drawable.ui_big_skewb, R.drawable.ui_huge_skewb} ,
+                       {3 , 17, 21, R.raw.skew3, R.drawable.ui_small_skewm, R.drawable.ui_medium_skewm, R.drawable.ui_big_skewm, R.drawable.ui_huge_skewm} ,
+                     },
+         5
+       ),
+
+  IVY  (
+         new int[][] {
+                       {2 , 8, 8, R.raw.ivy, R.drawable.ui_small_ivy, R.drawable.ui_medium_ivy, R.drawable.ui_big_ivy, R.drawable.ui_huge_ivy} ,
+                     },
+         5
+       ),
+
+  REX  (
+         new int[][] {
+                       {3 , 16, 19, R.raw.rex, R.drawable.ui_small_rex, R.drawable.ui_medium_rex, R.drawable.ui_big_rex, R.drawable.ui_huge_rex} ,
+                     },
+         5
+       ),
+
+  BAN1 (
+         new int[][] {
+                       {3 , 16, 16, R.raw.ban1, R.drawable.ui_small_ban1, R.drawable.ui_medium_ban1, R.drawable.ui_big_ban1, R.drawable.ui_huge_ban1} ,
+                     },
+         6
+       ),
+
+  BAN2 (
+         new int[][] {
+                       {3 , 16, 16, R.raw.ban2, R.drawable.ui_small_ban2, R.drawable.ui_medium_ban2, R.drawable.ui_big_ban2, R.drawable.ui_huge_ban2} ,
+                     },
+         6
+       ),
+
+  BAN3 (
+         new int[][] {
+                       {3 , 16, 16, R.raw.ban3, R.drawable.ui_small_ban3, R.drawable.ui_medium_ban3, R.drawable.ui_big_ban3, R.drawable.ui_huge_ban3} ,
+                     },
+         6
+       ),
+
+  BAN4 (
+         new int[][] {
+                       {3 , 16, 16, R.raw.ban4, R.drawable.ui_small_ban4, R.drawable.ui_medium_ban4, R.drawable.ui_big_ban4, R.drawable.ui_huge_ban4} ,
+                     },
+         6
+       ),
+
+  SQU1 (
+         new int[][] {
+                       {3 , 24, 24, R.raw.square1, R.drawable.ui_small_square1, R.drawable.ui_medium_square1, R.drawable.ui_big_square1, R.drawable.ui_huge_square1} ,
+                     },
+         7
+       ),
+
+  SQU2 (
+         new int[][] {
+                       {3 , 24, 24, R.raw.square2, R.drawable.ui_small_square2, R.drawable.ui_medium_square2, R.drawable.ui_big_square2, R.drawable.ui_huge_square2} ,
+                     },
+         7
+       ),
+
+  MIRR (
+         new int[][] {
+                       {2 , 12, 12, R.raw.mirr2, R.drawable.ui_small_mirr2, R.drawable.ui_medium_mirr2, R.drawable.ui_big_mirr2, R.drawable.ui_huge_mirr2} ,
+                       {3 , 16, 17, R.raw.mirr3, R.drawable.ui_small_mirr3, R.drawable.ui_medium_mirr3, R.drawable.ui_big_mirr3, R.drawable.ui_huge_mirr3} ,
+                     },
+         7
+       ),
+  ;
+
+  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_SCRAMBLE;
+  public static final int MAX_OBJECT_SIZE;
+
+  private final int[] mObjectSizes, mDBLevels, mNumScrambles, mSmallIconIDs, mMediumIconIDs, mBigIconIDs, mHugeIconIDs, mResourceIDs;
+  private final int mRow, 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 maxScramble= 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.mNumScrambles[j]> maxScramble ) maxScramble= object.mNumScrambles[j];
+        if( object.mDBLevels[j]    > maxLevel    ) maxLevel   = object.mDBLevels[j];
+        if( object.mObjectSizes[j] > maxSize     ) maxSize    = object.mObjectSizes[j];
+        }
+      }
+
+    MAX_NUM_OBJECTS = maxNum;
+    MAX_LEVEL       = maxLevel;
+    MAX_SCRAMBLE    = maxScramble;
+    MAX_OBJECT_SIZE = maxSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void setUpColAndRow()
+    {
+    mIndices = new int[NUM_OBJECTS];
+    mRowCount= 0;
+
+    for(int obj=0; obj<NUM_OBJECTS; obj++)
+      {
+      mIndices[obj] = objects[obj].mRow;
+      if( mIndices[obj]>=mRowCount ) mRowCount = mIndices[obj]+1;
+      }
+
+    mColCount = 0;
+
+    for(int row=0; row<mRowCount; row++)
+      {
+      int numObjects = computeNumObjectsInRow(row);
+      if( numObjects>mColCount ) mColCount = numObjects;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int computeNumObjectsInRow(int row)
+    {
+    int num=0;
+
+    for(int object=0; object<NUM_OBJECTS; object++)
+      {
+      if( objects[object].mRow == row )
+        {
+        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 getDBLevel(int ordinal, int sizeIndex)
+    {
+    if( ordinal>=0 && ordinal<NUM_OBJECTS )
+      {
+      int num = objects[ordinal].mObjectSizes.length;
+      return sizeIndex>=0 && sizeIndex<num ? objects[ordinal].mDBLevels[sizeIndex] : 0;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getNumScramble(int ordinal, int sizeIndex)
+    {
+    if( ordinal>=0 && ordinal<NUM_OBJECTS )
+      {
+      int num = objects[ordinal].mObjectSizes.length;
+      return sizeIndex>=0 && sizeIndex<num ? objects[ordinal].mNumScrambles[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;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ObjectList(int[][] info, int row)
+    {
+    mNumSizes = info.length;
+
+    mObjectSizes  = new int[mNumSizes];
+    mDBLevels     = new int[mNumSizes];
+    mNumScrambles = 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];
+      mDBLevels[i]     = info[i][1];
+      mNumScrambles[i] = info[i][2];
+      mResourceIDs[i]  = info[i][3];
+      mSmallIconIDs[i] = info[i][4];
+      mMediumIconIDs[i]= info[i][5];
+      mBigIconIDs[i]   = info[i][6];
+      mHugeIconIDs[i]  = info[i][7];
+      }
+
+    mRow  = row;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getSizes()
+    {
+    return mObjectSizes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 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 TwistyJing           (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  2: return new TwistyPyraminx       (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  3: return new TwistyKilominx       (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  4: return new TwistyMegaminx       (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  5: return new TwistyUltimate       (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  6: return new TwistyDiamond        (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  7: return new TwistyDino6          (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  8: return new TwistyDino4          (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case  9: return new TwistyRedi           (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 10: return new TwistyHelicopter     (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 11: return new TwistySkewb          (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 12: return new TwistyIvy            (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 13: return new TwistyRex            (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 14: return new TwistyBandagedFused  (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 15: return new TwistyBandaged2Bar   (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 16: return new TwistyBandaged3Plate (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 17: return new TwistyBandagedEvil   (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 18: return new TwistySquare1        (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 19: return new TwistySquare2        (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 20: return new TwistyMirror         (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      }
+
+    return null;
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/ObjectShape.java b/src/main/java/org/distorted/objectlb/ObjectShape.java
new file mode 100644
index 00000000..a1d8a84e
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/ObjectShape.java
@@ -0,0 +1,124 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ObjectShape
+  {
+  private final double[][] mVertices;
+  private final int[][] mVertIndices;
+  private final float[][] mBands;
+  private final int[] mBandIndices;
+  private final float[][] mCorners;
+  private final int[] mCornerIndices;
+  private final float[][] mCenters;
+  private final int[] mCenterIndices;
+  private final int mNumComponents;
+  private final float[] mConvexityCenter;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectShape(double[][] vertices, int[][] vertIndices, float[][] bands, int[] bandIndices,
+                     float[][] corners, int[] cornIndices, float[][] centers, int[] centIndices,
+                     int numComponents, float[] convexityCenter)
+    {
+    mVertices        = vertices;
+    mVertIndices     = vertIndices;
+    mBands           = bands;
+    mBandIndices     = bandIndices;
+    mCorners         = corners;
+    mCornerIndices   = cornIndices;
+    mCenters         = centers;
+    mCenterIndices   = centIndices;
+    mNumComponents   = numComponents;
+    mConvexityCenter = convexityCenter;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public double[][] getVertices()
+    {
+    return mVertices;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getVertIndices()
+    {
+    return mVertIndices;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[][] getBands()
+    {
+    return mBands;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getBandIndices()
+    {
+    return mBandIndices;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[][] getCorners()
+    {
+    return mCorners;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getCornerIndices()
+    {
+    return mCornerIndices;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[][] getCenters()
+    {
+    return mCenters;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getCenterIndices()
+    {
+    return mCenterIndices;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumComponents()
+    {
+    return mNumComponents;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getConvexityCenter()
+    {
+    return mConvexityCenter;
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/ObjectSticker.java b/src/main/java/org/distorted/objectlb/ObjectSticker.java
new file mode 100644
index 00000000..b0ac746a
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/ObjectSticker.java
@@ -0,0 +1,68 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ObjectSticker
+  {
+  private final float[] mCoords;
+  private final float[] mCurvature;
+  private final float[] mRadii;
+  private final float mStroke;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectSticker(float[] coords, float[] curvature, float[] radii, float stroke)
+    {
+    mCoords    = coords;
+    mCurvature = curvature;
+    mRadii     = radii;
+    mStroke    = stroke;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getCoords()
+    {
+    return mCoords;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getCurvature()
+    {
+    return mCurvature;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getRadii()
+    {
+    return mRadii;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getStroke()
+    {
+    return mStroke;
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/QuatHelper.java b/src/main/java/org/distorted/objectlb/QuatHelper.java
new file mode 100644
index 00000000..d06f10a3
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/QuatHelper.java
@@ -0,0 +1,163 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+import org.distorted.library.type.Static4D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class QuatHelper
+  {
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return quat1*quat2
+
+  public static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
+    {
+    float qx = quat1.get0();
+    float qy = quat1.get1();
+    float qz = quat1.get2();
+    float qw = quat1.get3();
+
+    float rx = quat2.get0();
+    float ry = quat2.get1();
+    float rz = quat2.get2();
+    float rw = quat2.get3();
+
+    float tx = rw*qx - rz*qy + ry*qz + rx*qw;
+    float ty = rw*qy + rz*qx + ry*qw - rx*qz;
+    float tz = rw*qz + rz*qw - ry*qx + rx*qy;
+    float tw = rw*qw - rz*qz - ry*qy - rx*qx;
+
+    return new Static4D(tx,ty,tz,tw);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
+
+  public static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
+    {
+    float qx = quat.get0();
+    float qy = quat.get1();
+    float qz = quat.get2();
+    float qw = quat.get3();
+
+    Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+    Static4D tmp = quatMultiply(quat,vector);
+
+    return quatMultiply(tmp,quatInverted);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
+
+  public static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
+    {
+    float qx = quat.get0();
+    float qy = quat.get1();
+    float qz = quat.get2();
+    float qw = quat.get3();
+
+    Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+    Static4D tmp = quatMultiply(quatInverted,vector);
+
+    return quatMultiply(tmp,quat);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static Static4D quatFromDrag(float dragX, float dragY)
+    {
+    float axisX = dragY;  // inverted X and Y - rotation axis is perpendicular to (dragX,dragY)
+    float axisY = dragX;  // Why not (-dragY, dragX) ? because Y axis is also inverted!
+    float axisZ = 0;
+    float axisL = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ);
+
+    if( axisL>0 )
+      {
+      axisX /= axisL;
+      axisY /= axisL;
+      axisZ /= axisL;
+
+      float ratio = axisL;
+      ratio = ratio - (int)ratio;     // the cos() is only valid in (0,Pi)
+
+      float cosA = (float)Math.cos(Math.PI*ratio);
+      float sinA = (float)Math.sqrt(1-cosA*cosA);
+
+      return new Static4D(axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
+      }
+
+    return new Static4D(0f, 0f, 0f, 1f);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static double computeCos(double oldX, double oldY, double newX, double newY, double len1, double len2)
+    {
+    double ret= (oldX*newX+oldY*newY) / (len1*len2);
+    if( ret<-1.0 ) return -1.0;
+    if( ret> 1.0 ) return  1.0;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// sin of (signed!) angle between vectors 'old' and 'new', counterclockwise!
+
+  public static double computeSin(double oldX, double oldY, double newX, double newY, double len1, double len2)
+    {
+    double ret= (newX*oldY-oldX*newY) / (len1*len2);
+    if( ret<-1.0 ) return -1.0;
+    if( ret> 1.0 ) return  1.0;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return quat Q that turns 3D vector A=(ax,ay,az) to another 3D vector B=(bx,by,bz)
+// take care of double-cover by ensuring that always Q.get3() >=0
+
+  public static Static4D retRotationQuat(float ax, float ay, float az, float bx, float by, float bz)
+    {
+    float nx = ay*bz - az*by;
+    float ny = az*bx - ax*bz;
+    float nz = ax*by - ay*bx;
+
+    float sin = (float)Math.sqrt(nx*nx + ny*ny + nz*nz);
+    float cos = ax*bx + ay*by + az*bz;
+
+    if( sin!=0 )
+      {
+      nx /= sin;
+      ny /= sin;
+      nz /= sin;
+      }
+
+    // Why sin<=0 and cos>=0 ?
+    // 0<angle<180 -> 0<halfAngle<90 -> both sin and cos are positive.
+    // But1: quats work counterclockwise -> negate cos.
+    // But2: double-cover, we prefer to have the cos positive (so that unit=(0,0,0,1))
+    // so negate again both cos and sin.
+    float sinHalf =-(float)Math.sqrt((1-cos)/2);
+    float cosHalf = (float)Math.sqrt((1+cos)/2);
+
+    return new Static4D(nx*sinHalf,ny*sinHalf,nz*sinHalf,cosHalf);
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlb/ScrambleState.java b/src/main/java/org/distorted/objectlb/ScrambleState.java
new file mode 100644
index 00000000..0efbf652
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/ScrambleState.java
@@ -0,0 +1,141 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+import java.util.Random;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScrambleState
+{
+  private final int mTotal, mNumAxis;
+  private final int[] mNum;
+  private final int[] mInfo;
+  private final int[] mTmp;
+  private final int LEN = 4;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScrambleState(int[][] axis)
+    {
+    mTmp = new int[LEN];
+
+    mNumAxis = axis.length;
+    mNum = new int[mNumAxis];
+    int total =0;
+
+    for(int i=0; i<mNumAxis; i++)
+      {
+      mNum[i] = axis[i]==null ? 0 : axis[i].length/(LEN-1);
+      total += mNum[i];
+      }
+
+    mTotal = total;
+
+    mInfo = new int[LEN*total];
+    int start = 0;
+
+    for(int i=0; i<mNumAxis; i++)
+      {
+      for(int j=0; j<mNum[i]; j++)
+        {
+        mInfo[LEN*j   + start] = i;
+        mInfo[LEN*j+1 + start] = axis[i][(LEN-1)*j  ];
+        mInfo[LEN*j+2 + start] = axis[i][(LEN-1)*j+1];
+        mInfo[LEN*j+3 + start] = axis[i][(LEN-1)*j+2];
+        }
+
+      start += LEN*mNum[i];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getIndex(int num, int indexExcluded)
+    {
+    int current= -1;
+
+    for(int i=0; i<mTotal; i++)
+      if( mInfo[LEN*i]!=indexExcluded && ++current==num ) return i;
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getRandom(Random rnd, int indexExcluded, int[][] scrambleTable, int[] numOccurences)
+    {
+    int num=0, total=0, max=0, ax, layer;
+
+    for(int i=0; i<mTotal; i++)
+      {
+      ax = mInfo[LEN*i];
+
+      if( ax!=indexExcluded )
+        {
+        layer = mInfo[LEN*i+1];
+        int value = scrambleTable[ax][layer];
+        if( value>max ) max=value;
+        }
+      }
+
+    for(int i=0; i<mTotal; i++)
+      {
+      ax = mInfo[LEN*i];
+
+      if( ax!=indexExcluded )
+        {
+        layer = mInfo[LEN*i+1];
+        int value = scrambleTable[ax][layer];
+        numOccurences[total] = 1 + max - value + (total==0 ? 0 : numOccurences[total-1]);
+        total++;
+        }
+      }
+
+    float random= rnd.nextFloat()*numOccurences[total-1];
+
+    for(int i=0; i<total; i++)
+      {
+      if( random <= numOccurences[i] )
+        {
+        num=i;
+        break;
+        }
+      }
+
+    int index = getIndex(num,indexExcluded);
+
+    mTmp[0] = mInfo[LEN*index  ];   // axis
+    mTmp[1] = mInfo[LEN*index+1];   // row
+    mTmp[2] = mInfo[LEN*index+2];   // angle
+    mTmp[3] = mInfo[LEN*index+3];   // next state
+
+    scrambleTable[mTmp[0]][mTmp[1]]++;
+
+    return mTmp;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getTotal(int indexExcluded)
+    {
+    return ( indexExcluded>=0 && indexExcluded<mNumAxis ) ? mTotal-mNum[indexExcluded] : mTotal;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/objectlb/ScrambleStateBandagedEvil.java b/src/main/java/org/distorted/objectlb/ScrambleStateBandagedEvil.java
new file mode 100644
index 00000000..da5cbb70
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/ScrambleStateBandagedEvil.java
@@ -0,0 +1,669 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// producer of the ScrambleStateGraph - but only for a single Twisty Puzzle, the 'BandagedEvil'.
+
+import java.util.ArrayList;
+
+public class ScrambleStateBandagedEvil
+{
+  private static final int INVALID_MOVE = -1;
+
+  private static final int CORNER_S = 0;
+  private static final int CORNER_X = 1;
+  private static final int CORNER_Y = 2;
+  private static final int CORNER_Z = 3;
+
+  private static final int CENTER_0 = 0;
+  private static final int CENTER_1 = 1;
+  private static final int CENTER_2 = 2;
+  private static final int CENTER_3 = 3;
+
+  private int mID;
+  private final int[] mMoves;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScrambleStateBandagedEvil(int id)
+    {
+    mID = id;
+    mMoves = createMoves(mID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void computeGraph()
+    {
+    ArrayList<ScrambleStateBandagedEvil> graph;
+
+    int id = 0;
+    int id1 = setCenter(id  , CENTER_2, 0);
+    int id2 = setCenter(id1 , CENTER_2, 1);
+    int id3 = setCenter(id2 , CENTER_3, 2);
+    int id4 = setCenter(id3 , CENTER_2, 3);
+
+    int id5 = setCorner(id4 , CORNER_X, 0);
+    int id6 = setCorner(id5 , CORNER_Y, 1);
+    int id7 = setCorner(id6 , CORNER_X, 2);
+    int id8 = setCorner(id7 , CORNER_Z, 3);
+    int id9 = setCorner(id8 , CORNER_Y, 4);
+    int id10= setCorner(id9 , CORNER_Y, 5);
+    int id11= setCorner(id10, CORNER_S, 6);
+    int id12= setCorner(id11, CORNER_Z, 7);
+
+    ScrambleStateBandagedEvil bsg = new ScrambleStateBandagedEvil(id12);
+    graph = new ArrayList<>();
+    graph.add(bsg);
+
+    insertChildren(graph,id12);
+    pruneGraph(graph);
+    remapGraph(graph);
+
+    int num = graph.size();
+    android.util.Log.e("D", "\n"+num+" states\n");
+
+    for(int i=0; i<num; i++)
+      {
+      bsg = graph.get(i);
+      android.util.Log.e("D", formatMoves(bsg));
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void insertChildren(ArrayList<ScrambleStateBandagedEvil> list, int id)
+    {
+    ScrambleStateBandagedEvil bsg = findState(list,id);
+
+    if( bsg==null )
+      {
+      android.util.Log.e("D", "error: "+id+" doesn't exist");
+      return;
+      }
+
+    for(int i=0; i<12; i++)
+      {
+      int move = bsg.getMove(i);
+
+      if( move!=INVALID_MOVE )
+        {
+        ScrambleStateBandagedEvil tmp = findState(list,move);
+
+        if( tmp==null )
+          {
+          tmp = new ScrambleStateBandagedEvil(move);
+          list.add(tmp);
+          insertChildren(list,move);
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void pruneGraph(ArrayList<ScrambleStateBandagedEvil> list)
+    {
+    int num = list.size(), numAxis;
+    boolean pruned = false;
+    ScrambleStateBandagedEvil bsg;
+
+    for(int i=0; i<num; i++)
+      {
+      bsg = list.get(i);
+      numAxis = bsg.numAxis();
+
+      if( numAxis<2 )
+        {
+        list.remove(i);
+        int id = bsg.getID();
+        pruned = true;
+        remapID(list,id,INVALID_MOVE);
+        break;
+        }
+      }
+
+    if( pruned ) pruneGraph(list);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void remapGraph(ArrayList<ScrambleStateBandagedEvil> list)
+    {
+    int id, num = list.size();
+    ScrambleStateBandagedEvil bsg;
+
+    for(int i=0; i<num; i++ )
+      {
+      bsg = list.get(i);
+      id = bsg.getID();
+      bsg.setID(i);
+      remapID(list,id,i);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void remapID(ArrayList<ScrambleStateBandagedEvil> list, int id, int newId)
+    {
+    ScrambleStateBandagedEvil bsg;
+    int size = list.size();
+
+    for(int i=0; i<size; i++)
+      {
+      bsg = list.get(i);
+
+      for(int j=0; j<12; j++)
+        {
+        if( bsg.getMove(j)==id ) bsg.setMove(j,newId);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static ScrambleStateBandagedEvil findState(ArrayList<ScrambleStateBandagedEvil> list, int id)
+    {
+    ScrambleStateBandagedEvil bsg;
+    int num = list.size();
+
+    for(int i=0; i<num; i++)
+      {
+      bsg= list.get(i);
+      if( bsg.getID() == id ) return bsg;
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String formatMoves(ScrambleStateBandagedEvil bsg)
+    {
+    String x = getTable(bsg,0);
+    String y = getTable(bsg,3);
+    String z = getTable(bsg,6);
+
+    return "    new ScrambleStateGraph( new int[][] { "+x+", "+y+", "+z+" } ),";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String getTable(ScrambleStateBandagedEvil sc, int index)
+    {
+    String ret = "";
+
+    if( index==0 || index==3 )
+      {
+      int m0 = sc.getMove(index  );
+      int m1 = sc.getMove(index+1);
+      int m2 = sc.getMove(index+2);
+
+      if( m0==INVALID_MOVE && m1==INVALID_MOVE && m2==INVALID_MOVE ) return formatL("{}");
+
+      if( m0!=INVALID_MOVE ) ret += formatRet(ret,2, 1,m0);
+      if( m1!=INVALID_MOVE ) ret += formatRet(ret,2, 2,m1);
+      if( m2!=INVALID_MOVE ) ret += formatRet(ret,2,-1,m2);
+      }
+    else
+      {
+      int m0 = sc.getMove(index  );
+      int m1 = sc.getMove(index+1);
+      int m2 = sc.getMove(index+2);
+      int m3 = sc.getMove(index+3);
+      int m4 = sc.getMove(index+4);
+      int m5 = sc.getMove(index+5);
+
+      if( m0==INVALID_MOVE && m1==INVALID_MOVE && m2==INVALID_MOVE &&
+          m3==INVALID_MOVE && m4==INVALID_MOVE && m5==INVALID_MOVE  )
+        {
+        return formatL("{}");
+        }
+
+      if( m0!=INVALID_MOVE ) ret += formatRet(ret,0, 1,m0);
+      if( m1!=INVALID_MOVE ) ret += formatRet(ret,0, 2,m1);
+      if( m2!=INVALID_MOVE ) ret += formatRet(ret,0,-1,m2);
+      if( m3!=INVALID_MOVE ) ret += formatRet(ret,2, 1,m3);
+      if( m4!=INVALID_MOVE ) ret += formatRet(ret,2, 2,m4);
+      if( m5!=INVALID_MOVE ) ret += formatRet(ret,2,-1,m5);
+      }
+
+    return formatL("{" + ret + "}");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String formatRet(String str, int row, int angle, int id)
+    {
+    String ret = str.length()!=0 ? ",":"";
+
+    ret += row;
+    ret += angle<0 ? "," : ", ";
+    ret += angle;
+
+         if( id< 10 ) ret += (",  "+id);
+    else if( id<100 ) ret += (", " +id);
+    else              ret += (","  +id);
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static final int LENGTH = 28;
+
+  private static String formatL(String input)
+    {
+    int len = input.length();
+    String ret = input;
+    for(int i=0 ;i<LENGTH-len; i++) ret += " ";
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getID()
+    {
+    return mID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setID(int id)
+    {
+    mID = id;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getMove(int index)
+    {
+    return (index>=0 && index<12) ? mMoves[index] : -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int numAxis()
+    {
+    int num = 0;
+
+    if( mMoves[ 0]!=INVALID_MOVE || mMoves[ 1]!=INVALID_MOVE || mMoves[ 2]!=INVALID_MOVE ) num++;
+    if( mMoves[ 3]!=INVALID_MOVE || mMoves[ 4]!=INVALID_MOVE || mMoves[ 5]!=INVALID_MOVE ) num++;
+    if( mMoves[ 6]!=INVALID_MOVE || mMoves[ 7]!=INVALID_MOVE || mMoves[ 8]!=INVALID_MOVE ) num++;
+    if( mMoves[ 9]!=INVALID_MOVE || mMoves[10]!=INVALID_MOVE || mMoves[11]!=INVALID_MOVE ) num++;
+
+    return num;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setMove(int index, int newMove)
+    {
+    if( index>=0 && index<12 ) mMoves[index] = newMove;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String debug(int id)
+    {
+    int ce0 = getCenter(id,0);
+    int ce1 = getCenter(id,1);
+    int ce2 = getCenter(id,2);
+    int ce3 = getCenter(id,3);
+
+    int co0 = getCorner(id,0);
+    int co1 = getCorner(id,1);
+    int co2 = getCorner(id,2);
+    int co3 = getCorner(id,3);
+    int co4 = getCorner(id,4);
+    int co5 = getCorner(id,5);
+    int co6 = getCorner(id,6);
+    int co7 = getCorner(id,7);
+
+    String center = centerString(ce0) + centerString(ce1) + centerString(ce2) + centerString(ce3);
+    String corner = cornerString(co0) + cornerString(co1) + cornerString(co2) + cornerString(co3) +
+                    cornerString(co4) + cornerString(co5) + cornerString(co6) + cornerString(co7);
+
+    return center + " -" + corner;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String centerString(int center)
+    {
+    return " "+center;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String cornerString(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return " S";
+      case CORNER_X: return " X";
+      case CORNER_Y: return " Y";
+      case CORNER_Z: return " Z";
+      }
+
+    return "?";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int[] createMoves(int id)
+    {
+    int[] ret = new int[12];
+
+    boolean moveX  = xPossible(id);
+    boolean moveY  = yPossible(id);
+    boolean moveZ0 = z0Possible(id);
+    boolean moveZ2 = z2Possible(id);
+
+    if( moveX ) createXmoves(id,ret);
+    else        { ret[ 0] = INVALID_MOVE; ret[ 1] = INVALID_MOVE; ret[ 2] = INVALID_MOVE; }
+    if( moveY ) createYmoves(id,ret);
+    else        { ret[ 3] = INVALID_MOVE; ret[ 4] = INVALID_MOVE; ret[ 5] = INVALID_MOVE; }
+    if( moveZ0) createZ0moves(id,ret);
+    else        { ret[ 6] = INVALID_MOVE; ret[ 7] = INVALID_MOVE; ret[ 8] = INVALID_MOVE; }
+    if( moveZ2) createZ2moves(id,ret);
+    else        { ret[ 9] = INVALID_MOVE; ret[10] = INVALID_MOVE; ret[11] = INVALID_MOVE; }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean xPossible(int id)
+    {
+    if( getCorner(id,4)==CORNER_X ) return false;
+    if( getCorner(id,5)==CORNER_X ) return false;
+    if( getCorner(id,6)==CORNER_X ) return false;
+    if( getCorner(id,7)==CORNER_X ) return false;
+
+    if( getCenter(id,1)==CENTER_1 ) return false;
+    if( getCenter(id,2)==CENTER_1 ) return false;
+    if( getCenter(id,3)==CENTER_1 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean yPossible(int id)
+    {
+    if( getCorner(id,2)==CORNER_Y ) return false;
+    if( getCorner(id,3)==CORNER_Y ) return false;
+    if( getCorner(id,6)==CORNER_Y ) return false;
+    if( getCorner(id,7)==CORNER_Y ) return false;
+
+    if( getCenter(id,0)==CENTER_0 ) return false;
+    if( getCenter(id,2)==CENTER_0 ) return false;
+    if( getCenter(id,3)==CENTER_0 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean z0Possible(int id)
+    {
+    if( getCorner(id,0)==CORNER_Z ) return false;
+    if( getCorner(id,2)==CORNER_Z ) return false;
+    if( getCorner(id,4)==CORNER_Z ) return false;
+    if( getCorner(id,6)==CORNER_Z ) return false;
+
+    if( getCenter(id,0)==CENTER_1 ) return false;
+    if( getCenter(id,1)==CENTER_0 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean z2Possible(int id)
+    {
+    if( getCorner(id,1)==CORNER_Z ) return false;
+    if( getCorner(id,3)==CORNER_Z ) return false;
+    if( getCorner(id,5)==CORNER_Z ) return false;
+    if( getCorner(id,7)==CORNER_Z ) return false;
+
+    if( getCenter(id,0)==CENTER_3 ) return false;
+    if( getCenter(id,1)==CENTER_2 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int getCorner(int id, int index)
+    {
+    return (id>>(14-2*index))&3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int getCenter(int id, int index)
+    {
+    return (id>>(22-2*index))&3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int setCorner(int id, int corner, int index)
+    {
+    return id + ((corner-getCorner(id,index))<<(14-2*index));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int setCenter(int id, int center, int index)
+    {
+    return id + ((center-getCenter(id,index))<<(22-2*index));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createXmoves(int id, int[] moves)
+    {
+    int id1 = rotateX(id);
+    moves[0] = id1;
+    int id2 = rotateX(id1);
+    moves[1] = id2;
+    int id3 = rotateX(id2);
+    moves[2] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createYmoves(int id, int[] moves)
+    {
+    int id1 = rotateY(id);
+    moves[3] = id1;
+    int id2 = rotateY(id1);
+    moves[4] = id2;
+    int id3 = rotateY(id2);
+    moves[5] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createZ0moves(int id, int[] moves)
+    {
+    int id1 = rotateZ0(id);
+    moves[6] = id1;
+    int id2 = rotateZ0(id1);
+    moves[7] = id2;
+    int id3 = rotateZ0(id2);
+    moves[8] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createZ2moves(int id, int[] moves)
+    {
+    int id1 = rotateZ2(id);
+    moves[ 9] = id1;
+    int id2 = rotateZ2(id1);
+    moves[10] = id2;
+    int id3 = rotateZ2(id2);
+    moves[11] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateX(int id)
+    {
+    int newCorner4 = rotCornerX(getCorner(id,5));
+    int newCorner5 = rotCornerX(getCorner(id,7));
+    int newCorner6 = rotCornerX(getCorner(id,4));
+    int newCorner7 = rotCornerX(getCorner(id,6));
+    int newCenter  = rotCenter (getCenter(id,0));
+
+    int id1 = setCorner(id ,newCorner4,4);
+    int id2 = setCorner(id1,newCorner5,5);
+    int id3 = setCorner(id2,newCorner6,6);
+    int id4 = setCorner(id3,newCorner7,7);
+    int id5 = setCenter(id4,newCenter ,0);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateY(int id)
+    {
+    int newCorner2 = rotCornerY(getCorner(id,6));
+    int newCorner3 = rotCornerY(getCorner(id,2));
+    int newCorner6 = rotCornerY(getCorner(id,7));
+    int newCorner7 = rotCornerY(getCorner(id,3));
+    int newCenter  = rotCenter (getCenter(id,1));
+
+    int id1 = setCorner(id ,newCorner2,2);
+    int id2 = setCorner(id1,newCorner3,3);
+    int id3 = setCorner(id2,newCorner6,6);
+    int id4 = setCorner(id3,newCorner7,7);
+    int id5 = setCenter(id4,newCenter ,1);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateZ0(int id)
+    {
+    int newCorner0 = rotCornerZ(getCorner(id,2));
+    int newCorner2 = rotCornerZ(getCorner(id,6));
+    int newCorner4 = rotCornerZ(getCorner(id,0));
+    int newCorner6 = rotCornerZ(getCorner(id,4));
+    int newCenter  = rotCenter (getCenter(id,2));
+
+    int id1 = setCorner(id ,newCorner0,0);
+    int id2 = setCorner(id1,newCorner2,2);
+    int id3 = setCorner(id2,newCorner4,4);
+    int id4 = setCorner(id3,newCorner6,6);
+    int id5 = setCenter(id4,newCenter ,2);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateZ2(int id)
+    {
+    int newCorner1 = rotCornerZ(getCorner(id,3));
+    int newCorner3 = rotCornerZ(getCorner(id,7));
+    int newCorner5 = rotCornerZ(getCorner(id,1));
+    int newCorner7 = rotCornerZ(getCorner(id,5));
+    int newCenter  = rotCenter (getCenter(id,3));
+
+    int id1 = setCorner(id ,newCorner1,1);
+    int id2 = setCorner(id1,newCorner3,3);
+    int id3 = setCorner(id2,newCorner5,5);
+    int id4 = setCorner(id3,newCorner7,7);
+    int id5 = setCenter(id4,newCenter ,3);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCornerX(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return CORNER_S;
+      case CORNER_X: android.util.Log.e("DIST", "rotateX: ERROR");
+                     return CORNER_S;
+      case CORNER_Y: return CORNER_Z;
+      case CORNER_Z: return CORNER_Y;
+      }
+
+    return CORNER_S;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCornerY(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return CORNER_S;
+      case CORNER_X: return CORNER_Z;
+      case CORNER_Y: android.util.Log.e("DIST", "rotateY: ERROR");
+                     return CORNER_S;
+      case CORNER_Z: return CORNER_X;
+      }
+
+    return CORNER_S;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCornerZ(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return CORNER_S;
+      case CORNER_X: return CORNER_Y;
+      case CORNER_Y: return CORNER_X;
+      case CORNER_Z: android.util.Log.e("DIST", "rotateZ: ERROR");
+                     return CORNER_S;
+      }
+
+    return CORNER_S;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCenter(int center)
+    {
+    switch(center)
+      {
+      case CENTER_0: return CENTER_3;
+      case CENTER_1: return CENTER_0;
+      case CENTER_2: return CENTER_1;
+      case CENTER_3: return CENTER_2;
+      }
+
+    return CENTER_0;
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/ScrambleStateSquare1.java b/src/main/java/org/distorted/objectlb/ScrambleStateSquare1.java
new file mode 100644
index 00000000..99dd997b
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/ScrambleStateSquare1.java
@@ -0,0 +1,593 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// producer of the ScrambleStateGraph - but only for a single Twisty Puzzle, the 'Square-1'.
+
+public class ScrambleStateSquare1
+{
+  private static final int INVALID_MOVE = -1;
+  private static final int NUM_MOVES = 23;
+
+  private int mDist;
+  private boolean mFresh;
+  private long mID;
+  private final long[] mMoves;
+
+  private final int[][] mPermittedAngles;
+  private final int[] mCornerQuat, mTmp;
+
+  // QUATS[i]*QUATS[j] = QUATS[QUAT_MULT[i][j]]
+  static final int[][] QUAT_MULT = new int[][]
+    {
+      {  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,},
+      {  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12,},
+      {  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13,},
+      {  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14,},
+      {  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15,},
+      {  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16,},
+      {  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17,},
+      {  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18,},
+      {  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19,},
+      {  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20,},
+      { 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,},
+      { 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,},
+      { 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,},
+      { 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,},
+      { 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,},
+      { 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,},
+      { 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,},
+      { 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,},
+      { 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,},
+      { 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,},
+      { 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,},
+      { 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,},
+      { 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11,},
+      { 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0,}
+    };
+
+  // quat indices that make corner cubits bandage the puzzle.
+  private static final int[][] BAD_CORNER_QUATS = new int[][]
+    {
+      { 2, 8,17,23},
+      { 5,11,14,20},
+    };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScrambleStateSquare1(long id, int dist)
+    {
+    mCornerQuat = new int[8];
+    mTmp        = new int[8];
+    mPermittedAngles = new int[2][12];
+
+    mDist = dist;
+    mFresh = true;
+    mID = id;
+    mMoves = createMoves(mID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setNotFresh()
+    {
+    mFresh = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean isFresh()
+    {
+    return mFresh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void computeGraph()
+    {
+    ArrayList<ScrambleStateSquare1> graph;
+
+    ScrambleStateSquare1 bsg = new ScrambleStateSquare1(0,0);
+    graph = new ArrayList<>();
+    graph.add(bsg);
+
+android.util.Log.e("D", "INSERTING");
+
+    insertChildren(graph,0);
+
+//android.util.Log.e("D", "PRUNING "+graph.size());
+
+//    pruneGraph(graph);
+
+android.util.Log.e("D", "REMAPPING "+graph.size());
+
+    remapGraph(graph);
+
+    int num = graph.size();
+    android.util.Log.e("D", "\n"+num+" states\n");
+
+    for(int i=0; i<num; i++)
+      {
+      bsg = graph.get(i);
+      android.util.Log.e("D", formatMoves(bsg));
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void insertChildren(ArrayList<ScrambleStateSquare1> list, long id)
+    {
+    ScrambleStateSquare1 bsg = findState(list,id);
+
+    if( bsg==null )
+      {
+      android.util.Log.e("D", "error: "+id+" doesn't exist");
+      return;
+      }
+
+    int dist = bsg.mDist;
+    if( dist>6 ) return;
+
+    for(int i=0; i<NUM_MOVES; i++)
+      {
+      long moveID = bsg.getMoveID(i);
+
+      if( moveID!=INVALID_MOVE )
+        {
+        ScrambleStateSquare1 tmp = findState(list,moveID);
+
+        if( tmp==null )
+          {
+          tmp = new ScrambleStateSquare1(moveID,dist+1);
+          list.add(tmp);
+          }
+        }
+      }
+
+    for(int i=0; i<NUM_MOVES; i++)
+      {
+      long moveID = bsg.getMoveID(i);
+
+      if( moveID!=INVALID_MOVE )
+        {
+        ScrambleStateSquare1 tmp = findState(list,moveID);
+
+        if( tmp.isFresh() )
+          {
+          tmp.setNotFresh();
+          insertChildren(list,moveID);
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void pruneGraph(ArrayList<ScrambleStateSquare1> list)
+    {
+    int num = list.size(), numAxis;
+    boolean pruned = false;
+    ScrambleStateSquare1 bsg;
+
+    for(int i=0; i<num; i++)
+      {
+      bsg = list.get(i);
+      numAxis = bsg.numAxis();
+
+      if( numAxis<2 )
+        {
+        list.remove(i);
+        long id = bsg.getID();
+        pruned = true;
+        remapID(list,id,INVALID_MOVE);
+        break;
+        }
+      }
+
+    if( pruned ) pruneGraph(list);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void remapGraph(ArrayList<ScrambleStateSquare1> list)
+    {
+    long id;
+    int num = list.size();
+    ScrambleStateSquare1 bsg;
+
+    for(int i=0; i<num; i++ )
+      {
+      bsg = list.get(i);
+      id = bsg.getID();
+      bsg.setID(i);
+      remapID(list,id,i);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void remapID(ArrayList<ScrambleStateSquare1> list, long id, long newId)
+    {
+    ScrambleStateSquare1 bsg;
+    int size = list.size();
+
+    for(int i=0; i<size; i++)
+      {
+      bsg = list.get(i);
+
+      for(int j=0; j<NUM_MOVES; j++)
+        {
+        if( bsg.getMoveID(j)==id ) bsg.setMoveID(j,newId);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static ScrambleStateSquare1 findState(ArrayList<ScrambleStateSquare1> list, long id)
+    {
+    ScrambleStateSquare1 bsg;
+    int num = list.size();
+
+    for(int i=0; i<num; i++)
+      {
+      bsg= list.get(i);
+      if( bsg.getID() == id ) return bsg;
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String formatMoves(ScrambleStateSquare1 bsg)
+    {
+    String x = getTable(bsg,0);
+    String y = getTable(bsg,11);
+    String z = getTable(bsg,12);
+
+    //return "    new ScrambleStateGraph( new int[][] { "+x+", "+y+", "+z+" } ),";
+
+    long id = bsg.getID();
+
+    return id+" "+x+" , "+y+" , "+z;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String getTable(ScrambleStateSquare1 sc, int index)
+    {
+    String ret = "";
+    long m;
+    int half = (NUM_MOVES)/2;
+
+    if( index==11 )
+      {
+      m = sc.getMoveID(index);
+      if( m==INVALID_MOVE ) return formatL("{}");
+      ret += formatRet(ret,0,1,m);
+      }
+    else
+      {
+      for(int i=0; i<half; i++)
+        {
+        m = sc.getMoveID(index+i);
+        int row = index==0 ? 0:2;
+        int angle = i+1;
+        if( m!=INVALID_MOVE ) ret += formatRet(ret,row,angle,m);
+        }
+      }
+
+    return formatL("{" + ret + "}");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String formatRet(String str, int row, int angle, long id)
+    {
+    String ret = str.length()!=0 ? "  ,":"  ";
+
+    ret += row;
+    ret += angle<0 ? "," : ",";
+    ret += angle;
+
+         if( id< 10 ) ret += (",  "+id);
+    else if( id<100 ) ret += (", " +id);
+    else              ret += (","  +id);
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static final int LENGTH = 28;
+
+  private static String formatL(String input)
+    {
+    int len = input.length();
+    String ret = input;
+    for(int i=0 ;i<LENGTH-len; i++) ret += " ";
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private long getID()
+    {
+    return mID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setID(long id)
+    {
+    mID = id;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private long getMoveID(int index)
+    {
+    return (index>=0 && index<NUM_MOVES) ? mMoves[index] : -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setMoveID(int index, long newID)
+    {
+    if( index>=0 && index<NUM_MOVES ) mMoves[index] = newID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int numAxis()
+    {
+    int num = 0;
+    int half = (NUM_MOVES)/2;
+
+    for(int i=0; i<half; i++)
+      {
+      if( mMoves[i]!=INVALID_MOVE )
+        {
+        num++;
+        break;
+        }
+      }
+
+    for(int i=half; i<NUM_MOVES-1; i++)
+      {
+      if( mMoves[i]!=INVALID_MOVE )
+        {
+        num++;
+        break;
+        }
+      }
+
+    if( mMoves[NUM_MOVES-1]!=INVALID_MOVE ) num++;
+
+    return num;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private long[] createMoves(long id)
+    {
+    long[] ret = new long[NUM_MOVES];
+    fillCornerQuat(id);
+    computePermittedAngles();
+
+    generateUMoves(ret, 0);
+    generateSMoves(ret,11);
+    generateLMoves(ret,12);
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void generateUMoves(long[] table, int index)
+    {
+    for(int angle=1; angle<12; angle++)
+      {
+      table[index+angle-1] = mPermittedAngles[1][angle]==1 ? updateCornerQuats(0,2,angle) : INVALID_MOVE;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void generateLMoves(long[] table, int index)
+    {
+    for(int angle=1; angle<12; angle++)
+      {
+      table[index+angle-1] = mPermittedAngles[0][angle]==1 ? updateCornerQuats(0,0,angle) : INVALID_MOVE;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void generateSMoves(long[] table, int index)
+    {
+    table[index] = updateCornerQuats(1,1,1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void fillCornerQuat(long id)
+    {
+    int NUM_QUATS = 24;
+
+    for(int i=0; i<8; i++)
+      {
+      mCornerQuat[i] = (int)(id%NUM_QUATS);
+      id/=NUM_QUATS;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static long returnID(int[] cornerQuat)
+    {
+    int NUM_QUATS = 24;
+    long mult=1,ret=0;
+
+    for(int i=0; i<8; i++)
+      {
+      ret += mult*cornerQuat[i];
+      mult*=NUM_QUATS;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean cornerIsUp(int index)
+    {
+    return ((index<4) ^ (mCornerQuat[index]>=12));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean cornerIsLeft(int index)
+    {
+    int q = mCornerQuat[index];
+
+    switch(index)
+      {
+      case 0:
+      case 4: return ((q>=3 && q<= 7) || (q>=18 && q<=22));
+      case 1:
+      case 5: return ((q>=6 && q<=10) || (q>=15 && q<=19));
+      case 2:
+      case 6: return ((q==0 || q==1 || (q>=9 && q<=11)) || (q>=12 && q<=16));
+      case 3:
+      case 7: return ((q>=0 && q<=4) || (q==12 || q==13 || (q>=21 && q<=23)));
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int makeQuat(int axis,int index)
+    {
+    if( axis==1 ) return 13;
+    if( index<0 ) index+=12;
+    return index;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean cornerBelongs(int index, int axis, int layer)
+    {
+    if( axis==0 )
+      {
+      boolean up = cornerIsUp(index);
+      return ((up && layer==2) || (!up && layer==0));
+      }
+    else
+      {
+      boolean le = cornerIsLeft(index);
+      return ((le && layer==0) || (!le && layer==1));
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private long updateCornerQuats(int axis, int layer, int index)
+    {
+    int quat = makeQuat(axis,index);
+
+    for(int corner=0; corner<8; corner++)
+      {
+      if( cornerBelongs(corner,axis,layer) )
+        {
+        int curr = mCornerQuat[corner];
+        mTmp[corner] = QUAT_MULT[quat][curr];
+        }
+      else
+        {
+        mTmp[corner] = mCornerQuat[corner];
+        }
+      }
+
+    return returnID(mTmp);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean quatIsBad(int quatIndex, int corner)
+    {
+    int index = (corner%2);
+
+    return ( quatIndex==BAD_CORNER_QUATS[index][0] ||
+             quatIndex==BAD_CORNER_QUATS[index][1] ||
+             quatIndex==BAD_CORNER_QUATS[index][2] ||
+             quatIndex==BAD_CORNER_QUATS[index][3]  );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int isPermittedDo(int angle)
+    {
+    for(int corner=0; corner<8; corner++)
+      {
+      if( !cornerIsUp(corner) )
+        {
+        int currQuat = mCornerQuat[corner];
+        int finalQuat= QUAT_MULT[angle][currQuat];
+        if( quatIsBad(finalQuat,corner) ) return 0;
+        }
+      }
+
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int isPermittedUp(int angle)
+    {
+    for(int corner=0; corner<8; corner++)
+      {
+      if( cornerIsUp(corner) )
+        {
+        int currQuat = mCornerQuat[corner];
+        int finalQuat= QUAT_MULT[angle][currQuat];
+        if( quatIsBad(finalQuat,corner) ) return 0;
+        }
+      }
+
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computePermittedAngles()
+    {
+    for(int angle=0; angle<12; angle++)
+      {
+      mPermittedAngles[0][angle] = isPermittedDo(angle);
+      mPermittedAngles[1][angle] = isPermittedUp(angle);
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Twisty12.java b/src/main/java/org/distorted/objectlb/Twisty12.java
new file mode 100644
index 00000000..bebc8fad
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Twisty12.java
@@ -0,0 +1,95 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+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;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class Twisty12 extends TwistyObject
+{
+  static final int MINX_LGREEN = 0xff53aa00;
+  static final int MINX_PINK   = 0xfffd7ab7;
+  static final int MINX_SANDY  = 0xffefd48b;
+  static final int MINX_LBLUE  = 0xff00a2d7;
+  static final int MINX_ORANGE = 0xffff6200;
+  static final int MINX_VIOLET = 0xff7d59a4;
+  static final int MINX_DGREEN = 0xff007a47;
+  static final int MINX_DRED   = 0xffbd0000;
+  static final int MINX_DBLUE  = 0xff1a29b2;
+  static final int MINX_DYELLOW= 0xffffc400;
+  static final int MINX_WHITE  = 0xffffffff;
+  static final int MINX_GREY   = 0xff727c7b;
+
+  static final int[] FACE_COLORS = new int[]
+         {
+           MINX_LGREEN, MINX_PINK   , MINX_SANDY , MINX_LBLUE,
+           MINX_ORANGE, MINX_VIOLET , MINX_DGREEN, MINX_DRED ,
+           MINX_DBLUE , MINX_DYELLOW, MINX_WHITE , MINX_GREY
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Twisty12(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+           DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
+    {
+    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getColor(int face)
+    {
+    return FACE_COLORS[face];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaceColors()
+    {
+    return 12;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFOV()
+    {
+    return 30;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.35f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 1.0f;
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Twisty4.java b/src/main/java/org/distorted/objectlb/Twisty4.java
new file mode 100644
index 00000000..26b6a1b8
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Twisty4.java
@@ -0,0 +1,81 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+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;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class Twisty4 extends TwistyObject
+{
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_GREEN , COLOR_YELLOW,
+           COLOR_BLUE  , COLOR_RED
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Twisty4(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+          DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
+    {
+    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getColor(int face)
+    {
+    return FACE_COLORS[face];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaceColors()
+    {
+    return 4;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFOV()
+    {
+    return 30;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.88f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return getNumLayers()/(SQ6/3);
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Twisty6.java b/src/main/java/org/distorted/objectlb/Twisty6.java
new file mode 100644
index 00000000..4adb102b
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Twisty6.java
@@ -0,0 +1,82 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+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;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class Twisty6 extends TwistyObject
+{
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_YELLOW, COLOR_WHITE,
+           COLOR_BLUE  , COLOR_GREEN,
+           COLOR_RED   , COLOR_ORANGE
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Twisty6(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+          DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
+    {
+    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getColor(int face)
+    {
+    return FACE_COLORS[face];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaceColors()
+    {
+    return 6;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFOV()
+    {
+    return 60;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.5f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return getNumLayers();
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/Twisty8.java b/src/main/java/org/distorted/objectlb/Twisty8.java
new file mode 100644
index 00000000..cb49f095
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/Twisty8.java
@@ -0,0 +1,83 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+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;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class Twisty8 extends TwistyObject
+{
+  private static final int[] FACE_COLORS = new int[]
+         {
+           COLOR_ORANGE, COLOR_VIOLET,
+           COLOR_WHITE , COLOR_BLUE  ,
+           COLOR_YELLOW, COLOR_RED   ,
+           COLOR_GREEN , COLOR_GREY
+         };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Twisty8(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+          DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
+    {
+    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getColor(int face)
+    {
+    return FACE_COLORS[face];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaceColors()
+    {
+    return 8;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFOV()
+    {
+    return 60;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 0.65f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 1.5f;
+    }
+}
diff --git a/src/main/java/org/distorted/objectlb/TwistyObject.java b/src/main/java/org/distorted/objectlb/TwistyObject.java
new file mode 100644
index 00000000..089f1277
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/TwistyObject.java
@@ -0,0 +1,1314 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.objectlb;
+
+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.DistortedLibrary;
+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 org.distorted.main.BuildConfig;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class TwistyObject extends DistortedNode
+  {
+  public static final int COLOR_YELLOW = 0xffffff00;
+  public static final int COLOR_WHITE  = 0xffffffff;
+  public static final int COLOR_BLUE   = 0xff0000ff;
+  public static final int COLOR_GREEN  = 0xff00bb00;
+  public static final int COLOR_RED    = 0xff990000;
+  public static final int COLOR_ORANGE = 0xffff6200;
+  public static final int COLOR_GREY   = 0xff727c7b;
+  public static final int COLOR_VIOLET = 0xff7700bb;
+  public static final int COLOR_BLACK  = 0xff000000;
+
+  public static final int TEXTURE_HEIGHT = 256;
+  static final int NUM_STICKERS_IN_ROW = 4;
+
+  public static final float SQ2 = (float)Math.sqrt(2);
+  public static final float SQ3 = (float)Math.sqrt(3);
+  public static final float SQ5 = (float)Math.sqrt(5);
+  public static final float SQ6 = (float)Math.sqrt(6);
+
+  private static final float NODE_RATIO = 1.60f;
+  private static final float MAX_SIZE_CHANGE = 1.35f;
+  private static final float MIN_SIZE_CHANGE = 0.75f;
+
+  private static final Static3D CENTER = new Static3D(0,0,0);
+  private static final int POST_ROTATION_MILLISEC = 500;
+
+  protected final int NUM_FACE_COLORS;
+  protected final int NUM_TEXTURES;
+  protected final Cubit[] CUBITS;
+
+  MeshBase[] mMeshes;
+  final Static4D[] OBJECT_QUATS;
+  final int NUM_CUBITS;
+  final int NUM_AXIS;
+  final int NUM_QUATS;
+
+  private final int mNumCubitFaces;
+  private final Static3D[] mAxis;
+  private final float[][] mCuts;
+  private final int[] mNumCuts;
+  private final int mNodeSize;
+  private final float[][] mOrigPos;
+  private final Static3D mNodeScale;
+  private final Static4D mQuat;
+  private final int mNumLayers, mRealSize;
+  private final ObjectList mList;
+  private final DistortedEffects mEffects;
+  private final VertexEffectRotate mRotateEffect;
+  private final Dynamic1D mRotationAngle;
+  private final Static3D mRotationAxis;
+  private final Static3D mObjectScale;
+  private final int[] mQuatDebug;
+  private final float mCameraDist;
+  private final Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
+  private final DistortedTexture mTexture;
+  private final float mInitScreenRatio;
+  private final int mSolvedFunctionIndex;
+  private final boolean mIsBandaged;
+  private float mObjectScreenRatio;
+  private int[][] mSolvedQuats;
+  private int[][] mQuatMult;
+  private int[] mTmpQuats;
+  private int mNumTexRows, mNumTexCols;
+  private int mRotRowBitmap;
+  private int mRotAxis;
+  private MeshBase mMesh;
+  private final TwistyObjectScrambler mScrambler;
+
+  //////////////////// SOLVED1 ////////////////////////
+
+  private int[] mFaceMap;
+  private int[][] mScramble;
+  private int[] mColors;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyObject(int numLayers, int realSize, 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));
+
+    mNumLayers = numLayers;
+    mRealSize = realSize;
+    mList = list;
+    mOrigPos = getCubitPositions(mNumLayers);
+    mAxis = getRotationAxis();
+    mInitScreenRatio = getScreenRatio();
+    mObjectScreenRatio = 1.0f;
+    mNumCubitFaces = getNumCubitFaces();
+    mSolvedFunctionIndex = getSolvedFunctionIndex();
+
+    mCuts = getCuts(mNumLayers);
+    mNumCuts = new int[mAxis.length];
+    if( mCuts==null ) for(int i=0; i<mAxis.length; i++) mNumCuts[i] = 0;
+    else              for(int i=0; i<mAxis.length; i++) mNumCuts[i] = mCuts[i].length;
+
+    OBJECT_QUATS = getQuats();
+    NUM_CUBITS  = mOrigPos.length;
+    NUM_FACE_COLORS = getNumFaceColors();
+    NUM_TEXTURES = getNumStickerTypes(mNumLayers)*NUM_FACE_COLORS;
+    NUM_AXIS = mAxis.length;
+    NUM_QUATS = OBJECT_QUATS.length;
+
+    int scramblingType = getScrambleType();
+    ScrambleState[] states = getScrambleStates();
+    mScrambler = new TwistyObjectScrambler(scramblingType,NUM_AXIS,numLayers,states);
+
+    boolean bandaged=false;
+
+    for(int c=0; c<NUM_CUBITS; c++)
+      {
+      if( mOrigPos[c].length>3 )
+        {
+        bandaged=true;
+        break;
+        }
+      }
+
+    mIsBandaged = bandaged;
+
+    mQuatDebug = new int[NUM_CUBITS];
+
+    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
+    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
+
+    mNodeScale= new Static3D(1,NODE_RATIO,1);
+    mQuat = quat;
+
+    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/mRealSize;
+    mObjectScale = new Static3D(scale,scale,scale);
+    MatrixEffectScale scaleEffect = new MatrixEffectScale(mObjectScale);
+    MatrixEffectQuaternion quatEffect  = new MatrixEffectQuaternion(quat, CENTER);
+
+    MatrixEffectScale nodeScaleEffect = new MatrixEffectScale(mNodeScale);
+    nodeEffects.apply(nodeScaleEffect);
+
+    mNumTexCols = NUM_STICKERS_IN_ROW;
+    mNumTexRows = (NUM_TEXTURES+1)/NUM_STICKERS_IN_ROW;
+
+    if( mNumTexCols*mNumTexRows < NUM_TEXTURES+1 ) mNumTexRows++;
+
+    CUBITS = new Cubit[NUM_CUBITS];
+    createMeshAndCubits(list,res);
+    createDataStructuresForSolved(numLayers);
+
+    mTexture = new DistortedTexture();
+    mEffects = new DistortedEffects();
+
+    for(int q=0; q<NUM_QUATS; q++)
+      {
+      VertexEffectQuaternion vq = new VertexEffectQuaternion(OBJECT_QUATS[q],CENTER);
+      vq.setMeshAssociation(0,q);
+      mEffects.apply(vq);
+      }
+
+    mEffects.apply(mRotateEffect);
+    mEffects.apply(quatEffect);
+    mEffects.apply(scaleEffect);
+
+    // 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);
+
+    float fov = getFOV();
+    double halfFOV = fov * (Math.PI/360);
+    mCameraDist = 0.5f*NODE_RATIO / (float)Math.tan(halfFOV);
+
+    setProjection( fov, 0.1f);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Static3D getPos(float[] origPos)
+    {
+    int len = origPos.length/3;
+    float sumX = 0.0f;
+    float sumY = 0.0f;
+    float sumZ = 0.0f;
+
+    for(int i=0; i<len; i++)
+      {
+      sumX += origPos[3*i  ];
+      sumY += origPos[3*i+1];
+      sumZ += origPos[3*i+2];
+      }
+
+    sumX /= len;
+    sumY /= len;
+    sumZ /= len;
+
+    return new Static3D(sumX,sumY,sumZ);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createMeshAndCubits(ObjectList list, Resources res)
+    {
+    int sizeIndex = ObjectList.getSizeIndex(list.ordinal(),mNumLayers);
+    int resourceID= list.getResourceIDs()[sizeIndex];
+
+    if( resourceID!=0 )
+      {
+      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], NUM_AXIS);
+        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], NUM_AXIS);
+        cubitMesh[i] = createCubitMesh(i,mNumLayers);
+        Static3D pos = getPos(mOrigPos[i]);
+        cubitMesh[i].apply(new MatrixEffectMove(pos),1,0);
+        cubitMesh[i].setEffectAssociation(0, CUBITS[i].computeAssociation(), 0);
+        }
+
+      mMesh = new MeshJoined(cubitMesh);
+      resetAllTextureMaps();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private MeshBase createCubitMesh(int cubit, int numLayers)
+    {
+    int variant = getCubitVariant(cubit,numLayers);
+
+    if( mMeshes==null )
+      {
+      FactoryCubit factory = FactoryCubit.getInstance();
+      factory.clear();
+      mMeshes = new MeshBase[getNumCubitVariants(numLayers)];
+      }
+
+    if( mMeshes[variant]==null )
+      {
+      ObjectShape shape = getObjectShape(cubit,numLayers);
+      FactoryCubit factory = FactoryCubit.getInstance();
+      factory.createNewFaceTransform(shape);
+      mMeshes[variant] = factory.createRoundedSolid(shape);
+      }
+
+    MeshBase mesh = mMeshes[variant].copy(true);
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( getQuat(cubit,numLayers), new Static3D(0,0,0) );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createDataStructuresForSolved(int numLayers)
+    {
+    mTmpQuats = new int[NUM_QUATS];
+    mSolvedQuats = new int[NUM_CUBITS][];
+
+    for(int c=0; c<NUM_CUBITS; c++)
+      {
+      mSolvedQuats[c] = getSolvedQuats(c,numLayers);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// This is used to build internal data structures for the generic 'isSolved()'
+//
+// if this is an internal cubit (all faces black): return -1
+// if this is a face cubit (one non-black face): return the color index of the only non-black face.
+// Color index, i.e. the index into the 'FACE_COLORS' table.
+// else (edge or corner cubit, more than one non-black face): return -2.
+
+  public int retCubitSolvedStatus(int cubit, int numLayers)
+    {
+    int numNonBlack=0, nonBlackIndex=-1, color;
+
+    for(int face=0; face<mNumCubitFaces; face++)
+      {
+      color = getFaceColor(cubit,face,numLayers);
+
+      if( color<NUM_TEXTURES )
+        {
+        numNonBlack++;
+        nonBlackIndex = color%NUM_FACE_COLORS;
+        }
+      }
+
+    if( numNonBlack==0 ) return -1;
+    if( numNonBlack>=2 ) return -2;
+
+    return nonBlackIndex;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] buildSolvedQuats(Static3D faceAx, Static4D[] quats)
+    {
+    final float MAXD = 0.0001f;
+    float x = faceAx.get0();
+    float y = faceAx.get1();
+    float z = faceAx.get2();
+    float a,dx,dy,dz,qx,qy,qz;
+    Static4D quat;
+
+    int len = quats.length;
+    int place = 0;
+
+    for(int q=1; q<len; q++)
+      {
+      quat = quats[q];
+      qx = quat.get0();
+      qy = quat.get1();
+      qz = quat.get2();
+
+           if( x!=0.0f ) { a = qx/x; }
+      else if( y!=0.0f ) { a = qy/y; }
+      else               { a = qz/z; }
+
+      dx = a*x-qx;
+      dy = a*y-qy;
+      dz = a*z-qz;
+
+      if( dx>-MAXD && dx<MAXD && dy>-MAXD && dy<MAXD && dz>-MAXD && dz<MAXD )
+        {
+        mTmpQuats[place++] = q;
+        }
+      }
+
+    if( place!=0 )
+      {
+      int[] ret = new int[place];
+      System.arraycopy(mTmpQuats,0,ret,0,place);
+      return ret;
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getMultQuat(int index1, int index2)
+    {
+    if( mQuatMult==null )
+      {
+      mQuatMult = new int[NUM_QUATS][NUM_QUATS];
+
+      for(int i=0; i<NUM_QUATS; i++)
+        for(int j=0; j<NUM_QUATS; j++) mQuatMult[i][j] = -1;
+      }
+
+    if( mQuatMult[index1][index2]==-1 )
+      {
+      mQuatMult[index1][index2] = mulQuat(index1,index2);
+      }
+
+    return mQuatMult[index1][index2];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isSolved()
+    {
+    if( mSolvedFunctionIndex==0 ) return isSolved0();
+    if( mSolvedFunctionIndex==1 ) return isSolved1();
+    if( mSolvedFunctionIndex==2 ) return isSolved2();
+    if( mSolvedFunctionIndex==3 ) return isSolved3();
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isSolved0()
+    {
+    int len, q1,q = CUBITS[0].mQuatIndex;
+    int[] solved;
+    boolean skip;
+
+    for(int c=1; c<NUM_CUBITS; c++)
+      {
+      q1 = CUBITS[c].mQuatIndex;
+
+      if( q1==q ) continue;
+
+      skip = false;
+      solved = mSolvedQuats[c];
+      len = solved==null ? 0:solved.length;
+
+      for(int i=0; i<len; i++)
+        {
+        if( q1==getMultQuat(q,solved[i]) )
+          {
+          skip = true;
+          break;
+          }
+        }
+
+      if( !skip ) return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeScramble(int quatNum, int centerNum)
+    {
+    float MAXDIFF = 0.01f;
+    float[] center= mOrigPos[centerNum];
+    Static4D sc = new Static4D(center[0], center[1], center[2], 1.0f);
+    Static4D result = QuatHelper.rotateVectorByQuat(sc,OBJECT_QUATS[quatNum]);
+
+    float x = result.get0();
+    float y = result.get1();
+    float z = result.get2();
+
+    for(int c=0; c<NUM_CUBITS; c++)
+      {
+      float[] cent = mOrigPos[c];
+
+      float qx = cent[0] - x;
+      float qy = cent[1] - y;
+      float qz = cent[2] - z;
+
+      if( qx>-MAXDIFF && qx<MAXDIFF &&
+          qy>-MAXDIFF && qy<MAXDIFF &&
+          qz>-MAXDIFF && qz<MAXDIFF  ) return c;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Dino4 uses this. It is solved if and only if groups of cubits
+// (0,3,7), (1,2,5), (4,8,9), (6,10,11)
+// or
+// (0,1,4), (2,3,6), (5,9,10), (7,8,11)
+// are all the same color.
+
+  public boolean isSolved1()
+    {
+    if( mScramble==null )
+      {
+      mScramble = new int[NUM_QUATS][NUM_CUBITS];
+      mColors   = new int[NUM_CUBITS];
+
+      for(int q=0; q<NUM_QUATS; q++)
+        for(int c=0; c<NUM_CUBITS; c++) mScramble[q][c] = computeScramble(q,c);
+      }
+
+    if( mFaceMap==null )
+      {
+      mFaceMap = new int[] { 4, 2, 2, 4, 0, 2, 1, 4, 0, 0, 1, 1 };
+      }
+
+    for(int c=0; c<NUM_CUBITS; c++)
+      {
+      int index = mScramble[CUBITS[c].mQuatIndex][c];
+      mColors[index] = mFaceMap[c];
+      }
+
+    if( mColors[0]==mColors[3] && mColors[0]==mColors[7] &&
+        mColors[1]==mColors[2] && mColors[1]==mColors[5] &&
+        mColors[4]==mColors[8] && mColors[4]==mColors[9]  ) return true;
+
+    if( mColors[0]==mColors[1] && mColors[0]==mColors[4] &&
+        mColors[2]==mColors[3] && mColors[2]==mColors[6] &&
+        mColors[5]==mColors[9] && mColors[5]==mColors[10] ) return true;
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Dino6 uses this. It 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 isSolved2()
+    {
+    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) );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Square-2 is solved iff
+// a) all of its cubits are rotated with the same quat
+// b) its two 'middle' cubits are rotated with the same quat, the 6 'front' and 6 'back'
+// edges and corners with this quat multiplied by QUATS[18] (i.e. those are upside down)
+// and all the 12 left and right edges and corners also with the same quat multiplied by
+// QUATS[12] - i.e. also upside down.
+
+  public boolean isSolved3()
+    {
+    int index = CUBITS[0].mQuatIndex;
+
+    if( CUBITS[1].mQuatIndex!=index ) return false;
+
+    boolean solved = true;
+
+    for(int i=2; i<NUM_CUBITS; i++)
+      {
+      if( CUBITS[i].mQuatIndex!=index )
+        {
+        solved = false;
+        break;
+        }
+      }
+
+    if( solved ) return true;
+
+    int indexX = mulQuat(index,12);  // QUATS[12] = 180deg (1,0,0)
+    int indexZ = mulQuat(index,18);  // QUATS[18] = 180deg (0,0,1)
+
+    for(int i= 2; i<        18; i+=2) if( CUBITS[i].mQuatIndex != indexZ ) return false;
+    for(int i= 3; i<        18; i+=2) if( CUBITS[i].mQuatIndex != indexX ) return false;
+    for(int i=18; i<NUM_CUBITS; i+=2) if( CUBITS[i].mQuatIndex != indexX ) return false;
+    for(int i=19; i<NUM_CUBITS; i+=2) if( CUBITS[i].mQuatIndex != indexZ ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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/mRealSize;
+    mObjectScale.set(scale,scale,scale);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getObjectRatio()
+    {
+    return mObjectScreenRatio*mInitScreenRatio;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int computeRow(float[] pos, int axisIndex)
+    {
+    int ret=0;
+    int len = pos.length / 3;
+    Static3D axis = mAxis[axisIndex];
+    float axisX = axis.get0();
+    float axisY = axis.get1();
+    float axisZ = axis.get2();
+    float casted;
+
+    for(int i=0; i<len; i++)
+      {
+      casted = pos[3*i]*axisX + pos[3*i+1]*axisY + pos[3*i+2]*axisZ;
+      ret |= computeSingleRow(axisIndex,casted);
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeSingleRow(int axisIndex,float casted)
+    {
+    int num = mNumCuts[axisIndex];
+
+    for(int i=0; i<num; i++)
+      {
+      if( casted<mCuts[axisIndex][i] ) return (1<<i);
+      }
+
+    return (1<<num);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean wasRotateApplied()
+    {
+    return mEffects.exists(mRotateEffect.getID());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean belongsToRotation( int cubit, int axis, int rowBitmap)
+    {
+    return (CUBITS[cubit].getRotRow(axis) & 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 = mAxis[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[] basic = getBasicAngle();
+
+      for(int[] move: moves)
+        {
+        axis     = move[0];
+        rowBitmap= move[1];
+        angle    = move[2]*(360/basic[axis]);
+        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);
+            }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getScrambleType()
+    {
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int computeBitmapFromRow(int rowBitmap, int axis)
+    {
+    if( mIsBandaged )
+      {
+      int bitmap, initBitmap=0;
+
+      while( initBitmap!=rowBitmap )
+        {
+        initBitmap = rowBitmap;
+
+        for(int cubit=0; cubit<NUM_CUBITS; cubit++)
+          {
+          bitmap = CUBITS[cubit].getRotRow(axis);
+          if( (rowBitmap & bitmap) != 0 ) rowBitmap |= bitmap;
+          }
+        }
+      }
+
+    return rowBitmap;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Clamp all rotated positions to one of those original ones to avoid accumulating errors.
+// Do so only if minimal Error is appropriately low (shape-shifting puzzles - Square-1)
+
+  void clampPos(float[] pos, int offset)
+    {
+    float currError, minError = Float.MAX_VALUE;
+    int minErrorIndex1 = -1;
+    int minErrorIndex2 = -1;
+
+    float x = pos[offset  ];
+    float y = pos[offset+1];
+    float z = pos[offset+2];
+
+    float xo,yo,zo;
+
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      int len = mOrigPos[i].length / 3;
+
+      for(int j=0; j<len; j++)
+        {
+        xo = mOrigPos[i][3*j  ];
+        yo = mOrigPos[i][3*j+1];
+        zo = mOrigPos[i][3*j+2];
+
+        currError = (xo-x)*(xo-x) + (yo-y)*(yo-y) + (zo-z)*(zo-z);
+
+        if( currError<minError )
+          {
+          minError = currError;
+          minErrorIndex1 = i;
+          minErrorIndex2 = j;
+          }
+        }
+      }
+
+    if( minError< 0.1f ) // TODO: 0.1 ?
+      {
+      pos[offset  ] = mOrigPos[minErrorIndex1][3*minErrorIndex2  ];
+      pos[offset+1] = mOrigPos[minErrorIndex1][3*minErrorIndex2+1];
+      pos[offset+2] = mOrigPos[minErrorIndex1][3*minErrorIndex2+2];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// remember about the double cover or unit quaternions!
+
+  int mulQuat(int q1, int q2)
+    {
+    Static4D result = QuatHelper.quatMultiply(OBJECT_QUATS[q1],OBJECT_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<NUM_QUATS; i++)
+      {
+      dX = OBJECT_QUATS[i].get0() - rX;
+      dY = OBJECT_QUATS[i].get1() - rY;
+      dZ = OBJECT_QUATS[i].get2() - rZ;
+      dW = OBJECT_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 = OBJECT_QUATS[i].get0() + rX;
+      dY = OBJECT_QUATS[i].get1() + rY;
+      dZ = OBJECT_QUATS[i].get2() + rZ;
+      dW = OBJECT_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;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitFaceColorIndex(int cubit, int face)
+    {
+    Static4D texMap = mMesh.getTextureMap(NUM_FACE_COLORS*cubit + face);
+
+    int x = (int)(texMap.get0()/texMap.get2());
+    int y = (int)(texMap.get1()/texMap.get3());
+
+    return (mNumTexRows-1-y)*NUM_STICKERS_IN_ROW + x;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// the getFaceColors + final black in a grid (so that we do not exceed the maximum texture size)
+
+  public void createTexture()
+    {
+    Bitmap bitmap;
+
+    Paint paint = new Paint();
+    bitmap = Bitmap.createBitmap( mNumTexCols*TEXTURE_HEIGHT, mNumTexRows*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(COLOR_BLACK);
+    canvas.drawRect(0, 0, mNumTexCols*TEXTURE_HEIGHT, mNumTexRows*TEXTURE_HEIGHT, paint);
+
+    int face = 0;
+    FactorySticker factory = FactorySticker.getInstance();
+
+    for(int row=0; row<mNumTexRows; row++)
+      for(int col=0; col<mNumTexCols; col++)
+        {
+        if( face>=NUM_TEXTURES ) break;
+        ObjectSticker sticker = retSticker(face);
+        factory.drawRoundedPolygon(canvas, paint, col*TEXTURE_HEIGHT, row*TEXTURE_HEIGHT, getColor(face%NUM_FACE_COLORS), sticker);
+        face++;
+        }
+
+    if( !mTexture.setTexture(bitmap) )
+      {
+      int max = DistortedLibrary.getMaxTextureSize();
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.log("failed to set texture of size "+bitmap.getWidth()+"x"+bitmap.getHeight()+" max is "+max);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumLayers()
+    {
+    return mNumLayers;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void continueRotation(float angleInDegrees)
+    {
+    mRotationAngleStatic.set0(angleInDegrees);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static4D getRotationQuat()
+      {
+      return mQuat;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void recomputeScaleFactor(int scrWidth)
+    {
+    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)
+    {
+    boolean error = false;
+
+    for(int i=0; i<NUM_CUBITS; i++)
+      {
+      mQuatDebug[i] = CUBITS[i].restorePreferences(preferences);
+
+      if( mQuatDebug[i]>=0 && mQuatDebug[i]<NUM_QUATS)
+        {
+        CUBITS[i].modifyCurrentPosition(OBJECT_QUATS[mQuatDebug[i]]);
+        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),mQuatDebug[i]);
+        }
+      else
+        {
+        error = true;
+        }
+      }
+
+    if( error )
+      {
+      for(int i=0; i<NUM_CUBITS; i++)
+        {
+        CUBITS[i].solve();
+        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),0);
+        }
+      recordQuatsState("Failed to restorePreferences");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void recordQuatsState(String message)
+    {
+    StringBuilder quats = new StringBuilder();
+
+    for(int j=0; j<NUM_CUBITS; j++)
+      {
+      quats.append(mQuatDebug[j]);
+      quats.append(" ");
+      }
+
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("quats" , quats.toString());
+      android.util.Log.e("object", mList.name()+"_"+mNumLayers);
+      }
+    else
+      {
+      Exception ex = new Exception(message);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("quats" , quats.toString());
+      crashlytics.setCustomKey("object", mList.name()+"_"+mNumLayers );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void releaseResources()
+    {
+    mTexture.markForDeletion();
+    mMesh.markForDeletion();
+    mEffects.markForDeletion();
+
+    for(int j=0; j<NUM_CUBITS; j++)
+      {
+      CUBITS[j].releaseResources();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 ratioW = 1.0f/mNumTexCols;
+    final float ratioH = 1.0f/mNumTexRows;
+    int color, row, col;
+
+    for(int cubit=0; cubit<NUM_CUBITS; cubit++)
+      {
+      final Static4D[] maps = new Static4D[mNumCubitFaces];
+
+      for(int cubitface=0; cubitface<mNumCubitFaces; cubitface++)
+        {
+        color = getFaceColor(cubit,cubitface,mNumLayers);
+        row = (mNumTexRows-1) - color/mNumTexCols;
+        col = color%mNumTexCols;
+        maps[cubitface] = new Static4D( col*ratioW, row*ratioH, ratioW, ratioH);
+        }
+
+      mMesh.setTextureMap(maps,mNumCubitFaces*cubit);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setTextureMap(int cubit, int face, int newColor)
+    {
+    final float ratioW = 1.0f/mNumTexCols;
+    final float ratioH = 1.0f/mNumTexRows;
+    final Static4D[] maps = new Static4D[mNumCubitFaces];
+    int row = (mNumTexRows-1) - newColor/mNumTexCols;
+    int col = newColor%mNumTexCols;
+
+    maps[face] = new Static4D( col*ratioW, row*ratioH, ratioW, ratioH);
+    mMesh.setTextureMap(maps,mNumCubitFaces*cubit);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void beginNewRotation(int axis, int row )
+    {
+    if( axis<0 || axis>=NUM_AXIS )
+      {
+      android.util.Log.e("object", "invalid rotation axis: "+axis);
+      return;
+      }
+    if( row<0 || row>=mNumLayers )
+      {
+      android.util.Log.e("object", "invalid rotation row: "+row);
+      return;
+      }
+
+    mRotAxis     = axis;
+    mRotRowBitmap= computeBitmapFromRow( (1<<row),axis );
+    mRotationAngleStatic.set0(0.0f);
+    mRotationAxis.set( mAxis[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 )
+    {
+    if( wasRotateApplied() )
+      {
+      mRotAxis     = axis;
+      mRotRowBitmap= computeBitmapFromRow( rowBitmap,axis );
+
+      mRotationAngleStatic.set0(0.0f);
+      mRotationAxis.set( mAxis[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();
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public long finishRotationNow(EffectListener listener, int nearestAngleInDegrees)
+    {
+    if( wasRotateApplied() )
+      {
+      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();
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 = mAxis[mRotAxis].get0();
+    float axisY = mAxis[mRotAxis].get1();
+    float axisZ = mAxis[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(int axis, float angle, float speed)
+    {
+    int[] basicArray = getBasicAngle();
+    int basicAngle   = basicArray[axis>=basicArray.length ? 0 : axis];
+    int nearestAngle = 360/basicAngle;
+
+    int tmp = (int)((angle+nearestAngle/2)/nearestAngle);
+    if( angle< -(nearestAngle*0.5) ) tmp-=1;
+
+    if( tmp!=0 ) return nearestAngle*tmp;
+
+    return speed> 1.2f ? nearestAngle*(angle>0 ? 1:-1) : 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getCameraDist()
+    {
+    return mCameraDist;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNodeSize()
+    {
+    return mNodeSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectList getObjectList()
+    {
+    return mList;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void randomizeNewScramble(int[][] scramble, Random rnd, int curr, int total)
+    {
+    mScrambler.randomizeNewScramble(scramble,rnd,curr,total);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract int getFOV();
+  abstract int getNumFaceColors();
+  abstract int getColor(int face);
+  abstract float returnMultiplier();
+  abstract float getScreenRatio();
+
+  protected abstract float[][] getCuts(int numLayers);
+  protected abstract int getNumCubitFaces();
+  protected abstract Static4D[] getQuats();
+  protected abstract float[][] getCubitPositions(int numLayers);
+  protected abstract int getCubitVariant(int cubit, int numLayers);
+  protected abstract int getNumCubitVariants(int numLayers);
+  protected abstract Static4D getQuat(int cubit, int numLayers);
+  protected abstract ObjectShape getObjectShape(int cubit, int numLayers);
+  protected abstract int[] getSolvedQuats(int cubit, int numLayers);
+  protected abstract int getSolvedFunctionIndex();
+  protected abstract ScrambleState[] getScrambleStates();
+  protected abstract int getNumStickerTypes(int numLayers);
+  protected abstract ObjectSticker retSticker(int face);
+  protected abstract int getFaceColor(int cubit, int cubitface, int numLayers);
+
+  public abstract Movement getMovement();
+  public abstract Static3D[] getRotationAxis();
+  public abstract int[] getBasicAngle();
+  public abstract int getObjectName(int numLayers);
+  public abstract int getInventor(int numLayers);
+  public abstract int getComplexity(int numLayers);
+  }
diff --git a/src/main/java/org/distorted/objectlb/TwistyObjectScrambler.java b/src/main/java/org/distorted/objectlb/TwistyObjectScrambler.java
new file mode 100644
index 00000000..11d95644
--- /dev/null
+++ b/src/main/java/org/distorted/objectlb/TwistyObjectScrambler.java
@@ -0,0 +1,392 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlb;
+
+import java.util.Random;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyObjectScrambler
+  {
+  private final ScrambleState[] mStates;
+  private final int mType;
+  private final int mNumAxis;
+  private final int mNumLayers;
+
+  // type=0, i.e. main
+  private int mCurrState;
+  private int mIndexExcluded;
+  private int[][] mScrambleTable;
+  private int[] mNumOccurences;
+
+  // type=1, i.e. the exception: Square-1
+  private static final int BASIC_ANGLE = 12;
+  private static final int LAST_SL = 0; // automatic rotations: last rot was a 'slash' i.e. along ROT_AXIS[1]
+  private static final int LAST_UP = 1; // last rot was along ROT_AXIS[0], upper layer and forelast was a slash
+  private static final int LAST_LO = 2; // last rot was along ROT_AXIS[0], lower layer and forelast was a slash
+  private static final int LAST_UL = 3; // two last rots were along ROT_AXIS[0] (so the next must be a slash)
+
+  private int[][] mPermittedAngles;
+  private int[] mCornerQuat;
+  private int mPermittedUp, mPermittedDo;
+  private int[][] mBadCornerQuats;
+  private int mLastRot;
+  private int[][] mQuatMult;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyObjectScrambler(int type, int numAxis, int numLayers, ScrambleState[] states)
+    {
+    mType = type;
+    mNumAxis = numAxis;
+    mNumLayers = numLayers;
+    mStates = states;
+
+    if( mType==1 )
+      {
+      mPermittedAngles = new int[2][BASIC_ANGLE];
+      mCornerQuat = new int[8];
+      mLastRot = LAST_SL;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// QUATS[i]*QUATS[j] = QUATS[QUAT_MULT[i][j]]
+
+  void initializeQuatMult()
+    {
+    mQuatMult = new int[][]
+      {
+        {  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,},
+        {  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12,},
+        {  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13,},
+        {  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14,},
+        {  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15,},
+        {  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16,},
+        {  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17,},
+        {  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18,},
+        {  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19,},
+        {  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20,},
+        { 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,},
+        { 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,},
+        { 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,},
+        { 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,},
+        { 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,},
+        { 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,},
+        { 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,},
+        { 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,},
+        { 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,},
+        { 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,},
+        { 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,},
+        { 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,},
+        { 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11,},
+        { 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0,}
+      };
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void initializeScrambling()
+    {
+    if( mScrambleTable ==null )
+      {
+      mScrambleTable = new int[mNumAxis][mNumLayers];
+      }
+    if( mNumOccurences ==null )
+      {
+      int max=0;
+
+      for (ScrambleState mState : mStates)
+        {
+        int tmp = mState.getTotal(-1);
+        if (max < tmp) max = tmp;
+        }
+
+      mNumOccurences = new int[max];
+      }
+
+    for(int i=0; i<mNumAxis; i++)
+      for(int j=0; j<mNumLayers; j++) mScrambleTable[i][j] = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  private void randomizeNewScramble0(int[][] scramble, Random rnd, int curr, int total)
+    {
+    if( curr==0 )
+      {
+      mCurrState     = 0;
+      mIndexExcluded =-1;
+      initializeScrambling();
+      }
+
+    int[] info= mStates[mCurrState].getRandom(rnd, mIndexExcluded, mScrambleTable, mNumOccurences);
+
+    scramble[curr][0] = info[0];
+    scramble[curr][1] = info[1];
+    scramble[curr][2] = info[2];
+
+    mCurrState     = info[3];
+    mIndexExcluded = info[0];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean cornerIsUp(int index)
+    {
+    return ((index<4) ^ (mCornerQuat[index]>=12));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean cornerIsLeft(int index)
+    {
+    int q = mCornerQuat[index];
+
+    switch(index)
+      {
+      case 0:
+      case 4: return ((q>=3 && q<= 7) || (q>=18 && q<=22));
+      case 1:
+      case 5: return ((q>=6 && q<=10) || (q>=15 && q<=19));
+      case 2:
+      case 6: return ((q==0 || q==1 || (q>=9 && q<=11)) || (q>=12 && q<=16));
+      case 3:
+      case 7: return ((q>=0 && q<=4) || (q==12 || q==13 || (q>=21 && q<=23)));
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean quatIsBad(int quatIndex, int corner)
+    {
+    if( mBadCornerQuats ==null )
+      {
+      // quat indices that make corner cubits bandage the puzzle.
+      mBadCornerQuats = new int[][] { { 2, 8,17,23}, { 5,11,14,20} };
+      }
+
+    int index = (corner%2);
+
+    return ( quatIndex== mBadCornerQuats[index][0] ||
+             quatIndex== mBadCornerQuats[index][1] ||
+             quatIndex== mBadCornerQuats[index][2] ||
+             quatIndex== mBadCornerQuats[index][3]  );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean isPermittedDo(int angle)
+    {
+    if( mQuatMult==null ) initializeQuatMult();
+
+    for(int corner=0; corner<8; corner++)
+      {
+      if( !cornerIsUp(corner) )
+        {
+        int currQuat = mCornerQuat[corner];
+        int finalQuat= mQuatMult[angle][currQuat];
+        if( quatIsBad(finalQuat,corner) ) return false;
+        }
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean isPermittedUp(int angle)
+    {
+    if( mQuatMult==null ) initializeQuatMult();
+
+    for(int corner=0; corner<8; corner++)
+      {
+      if( cornerIsUp(corner) )
+        {
+        int currQuat = mCornerQuat[corner];
+        int finalQuat= mQuatMult[angle][currQuat];
+        if( quatIsBad(finalQuat,corner) ) return false;
+        }
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computePermittedAngles()
+    {
+    mPermittedDo = 0;
+
+    for(int angle=0; angle<BASIC_ANGLE; angle++)
+      {
+      if( isPermittedDo(angle ) ) mPermittedAngles[0][mPermittedDo++] = angle;
+      }
+
+    mPermittedUp = 0;
+
+    for(int angle=0; angle<BASIC_ANGLE; angle++)
+      {
+      if( isPermittedUp(angle ) ) mPermittedAngles[1][mPermittedUp++] = angle;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getNextAngle(Random rnd, int layer)
+    {
+    int num = layer==0 ? mPermittedDo:mPermittedUp;
+    int index = rnd.nextInt(num);
+    int angle = mPermittedAngles[layer][index];
+    return angle<BASIC_ANGLE/2 ? -angle : BASIC_ANGLE-angle;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getNextAngleNotZero(Random rnd, int layer)
+    {
+    int num = layer==0 ? mPermittedDo:mPermittedUp;
+    int index = rnd.nextInt(num-1);
+    int angle = mPermittedAngles[layer][index+1];
+    return angle<BASIC_ANGLE/2 ? -angle : BASIC_ANGLE-angle;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int makeQuat(int axis,int index)
+    {
+    if( axis==1 ) return 13;
+    if( index<0 ) index+=12;
+    return index;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean cornerBelongs(int index, int axis, int layer)
+    {
+    if( axis==0 )
+      {
+      boolean up = cornerIsUp(index);
+      return ((up && layer==2) || (!up && layer==0));
+      }
+    else
+      {
+      boolean le = cornerIsLeft(index);
+      return ((le && layer==0) || (!le && layer==1));
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void updateCornerQuats(int[] rotInfo)
+    {
+    if( mQuatMult==null ) initializeQuatMult();
+
+    int axis = rotInfo[0];
+    int layer= rotInfo[1];
+    int index=-rotInfo[2];
+
+    int quat = makeQuat(axis,index);
+
+    for(int corner=0; corner<8; corner++)
+      {
+      if( cornerBelongs(corner,axis,layer) )
+        {
+        int curr = mCornerQuat[corner];
+        mCornerQuat[corner] = mQuatMult[quat][curr];
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void randomizeNewScramble1(int[][] scramble, Random rnd, int curr, int total)
+    {
+    int layer, nextAngle;
+
+    if( curr==0 )
+      {
+      for(int corner=0; corner<8; corner++) mCornerQuat[corner] = 0;
+      mLastRot = rnd.nextInt(4);
+      computePermittedAngles();
+      }
+
+    switch(mLastRot)
+      {
+      case LAST_SL: layer = rnd.nextInt(2);
+                    nextAngle = getNextAngle(rnd,layer);
+
+                    if( nextAngle==0 )
+                      {
+                      layer = 1-layer;
+                      nextAngle = getNextAngleNotZero(rnd,layer);
+                      }
+
+                    scramble[curr][0] = 0;
+                    scramble[curr][1] = 2*layer;
+                    scramble[curr][2] = nextAngle;
+                    mLastRot = layer==0 ? LAST_LO : LAST_UP;
+                    updateCornerQuats(scramble[curr]);
+                    break;
+      case LAST_LO:
+      case LAST_UP: layer = mLastRot==LAST_LO ? 1:0;
+                    nextAngle = getNextAngle(rnd,layer);
+
+                    if( nextAngle!=0 )
+                      {
+                      scramble[curr][0] = 0;
+                      scramble[curr][1] = 2*layer;
+                      scramble[curr][2] = nextAngle;
+                      updateCornerQuats(scramble[curr]);
+                      mLastRot = LAST_UL;
+                      }
+                    else
+                      {
+                      scramble[curr][0] = 1;
+                      scramble[curr][1] = rnd.nextInt(2);
+                      scramble[curr][2] = 1;
+                      mLastRot = LAST_SL;
+                      updateCornerQuats(scramble[curr]);
+                      computePermittedAngles();
+                      }
+
+                    break;
+      case LAST_UL: scramble[curr][0] = 1;
+                    scramble[curr][1] = rnd.nextInt(2);
+                    scramble[curr][2] = 1;
+                    mLastRot = LAST_SL;
+                    updateCornerQuats(scramble[curr]);
+                    computePermittedAngles();
+                    break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public void randomizeNewScramble(int[][] scramble, Random rnd, int curr, int total)
+    {
+    if( mType==0 ) randomizeNewScramble0(scramble, rnd, curr, total);
+    if( mType==1 ) randomizeNewScramble1(scramble, rnd, curr, total);
+    }
+  }
diff --git a/src/main/java/org/distorted/objects/Cubit.java b/src/main/java/org/distorted/objects/Cubit.java
deleted file mode 100644
index 02207cbd..00000000
--- a/src/main/java/org/distorted/objects/Cubit.java
+++ /dev/null
@@ -1,221 +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.SharedPreferences;
-
-import org.distorted.helpers.QuatHelper;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class Cubit
-  {
-  private final float[] mOrigPosition;
-  private final float[] mCurrentPosition;
-  private TwistyObject mParent;
-  private final int mNumAxis;
-  private final int mLen;
-
-  int mQuatIndex;
-  int[] mRotationRow;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Cubit(TwistyObject parent, float[] position, int numAxis)
-    {
-    mQuatIndex= 0;
-    mParent   = parent;
-    mLen      = position.length;
-
-    mOrigPosition    = new float[mLen];
-    mCurrentPosition = new float[mLen];
-
-    for(int i=0; i<mLen; i++)
-      {
-      mOrigPosition[i]    = position[i];
-      mCurrentPosition[i] = position[i];
-      }
-
-    mNumAxis     = numAxis;
-    mRotationRow = new int[mNumAxis];
-    computeRotationRow();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Because of quatMultiplication, errors can accumulate - so to avoid this, we
-// correct the value of the 'scramble' quat to what it should be - one of the legal quats from the
-// list QUATS.
-//
-// We also have to remember that the group of unit quaternions is a double-cover of rotations
-// in 3D ( q represents the same rotation as -q ) - so invert if needed.
-
-  private int normalizeScrambleQuat(Static4D quat)
-    {
-    float x = quat.get0();
-    float y = quat.get1();
-    float z = quat.get2();
-    float w = quat.get3();
-    float xd,yd,zd,wd;
-    float diff, mindiff = Float.MAX_VALUE;
-    int ret=0;
-    int num_quats = mParent.OBJECT_QUATS.length;
-    Static4D qt;
-
-    for(int q=0; q<num_quats; q++)
-      {
-      qt = mParent.OBJECT_QUATS[q];
-
-      xd = x - qt.get0();
-      yd = y - qt.get1();
-      zd = z - qt.get2();
-      wd = w - qt.get3();
-
-      diff = xd*xd + yd*yd + zd*zd + wd*wd;
-
-      if( diff < mindiff )
-        {
-        ret = q;
-        mindiff = diff;
-        }
-
-      xd = x + qt.get0();
-      yd = y + qt.get1();
-      zd = z + qt.get2();
-      wd = w + qt.get3();
-
-      diff = xd*xd + yd*yd + zd*zd + wd*wd;
-
-      if( diff < mindiff )
-        {
-        ret = q;
-        mindiff = diff;
-        }
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void computeRotationRow()
-    {
-    for(int i=0; i<mNumAxis; i++)
-      {
-      mRotationRow[i] = mParent.computeRow(mCurrentPosition,i);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void modifyCurrentPosition(Static4D quat)
-    {
-    Static4D cubitCenter;
-    Static4D rotatedCenter;
-    int len = mLen/3;
-
-    for(int i=0; i<len; i++)
-      {
-      cubitCenter =  new Static4D(mCurrentPosition[3*i], mCurrentPosition[3*i+1], mCurrentPosition[3*i+2], 0);
-      rotatedCenter = QuatHelper.rotateVectorByQuat( cubitCenter, quat);
-
-      mCurrentPosition[3*i  ] = rotatedCenter.get0();
-      mCurrentPosition[3*i+1] = rotatedCenter.get1();
-      mCurrentPosition[3*i+2] = rotatedCenter.get2();
-
-      mParent.clampPos(mCurrentPosition, 3*i);
-      }
-
-    computeRotationRow();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int computeAssociation()
-    {
-    int result = 0, accumulativeShift = 0;
-
-    for(int axis=0; axis<mNumAxis; axis++)
-      {
-      result += (mRotationRow[axis]<<accumulativeShift);
-      accumulativeShift += ObjectList.MAX_OBJECT_SIZE;
-      }
-
-    return result;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void savePreferences(SharedPreferences.Editor editor)
-    {
-    String number = mOrigPosition[0]+"_"+mOrigPosition[1]+"_"+mOrigPosition[2];
-    editor.putInt("q_"+number, mQuatIndex);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int restorePreferences(SharedPreferences preferences)
-    {
-    String number = mOrigPosition[0]+"_"+mOrigPosition[1]+"_"+mOrigPosition[2];
-    mQuatIndex = preferences.getInt("q_"+number, 0);
-    return mQuatIndex;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int removeRotationNow(Static4D quat)
-    {
-    Static4D q = QuatHelper.quatMultiply(quat,mParent.OBJECT_QUATS[mQuatIndex]);
-    mQuatIndex = normalizeScrambleQuat(q);
-
-    modifyCurrentPosition(quat);
-
-    return mQuatIndex;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void solve()
-    {
-    mQuatIndex = 0;
-    System.arraycopy(mOrigPosition, 0, mCurrentPosition, 0, mCurrentPosition.length);
-    computeRotationRow();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void releaseResources()
-    {
-    mParent = null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// this is only needed for MODE_REPLACE objects (i.e. - currently - CUBE_3), so it is enough to only
-// take into consideration the first position.
-
-  float getDistSquared(float[] point)
-    {
-    float dx = mCurrentPosition[0] - point[0];
-    float dy = mCurrentPosition[1] - point[1];
-    float dz = mCurrentPosition[2] - point[2];
-
-    return dx*dx + dy*dy + dz*dz;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/objects/Movement.java b/src/main/java/org/distorted/objects/Movement.java
deleted file mode 100644
index be8cff97..00000000
--- a/src/main/java/org/distorted/objects/Movement.java
+++ /dev/null
@@ -1,477 +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 org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class Movement
-  {
-  // it doesn't matter where we touch a face - the list of enabled rotAxis will always be the same
-  static final int TYPE_NOT_SPLIT    = 0;
-  // each face is split into several parts by lines coming from its center to the midpoints of each edge
-  static final int TYPE_SPLIT_EDGE   = 1;
-  // each face is split into several parts by lines coming from its center to the vertices
-  static final int TYPE_SPLIT_CORNER = 2;
-
-  static final float SQ3 = (float)Math.sqrt(3);
-  static final float SQ6 = (float)Math.sqrt(6);
-
-  private final int mNumFaceAxis;
-  private final float[] mPoint, mCamera, mTouch;
-  private final float[] mPoint2D, mMove2D;
-  private final int[] mEnabledRotAxis;
-  private final float mDistanceCenterFace3D;
-  private final Static3D[] mFaceAxis;
-
-  private int mLastTouchedFace;
-  private float[][][] mCastedRotAxis;
-  private Static4D[][] mCastedRotAxis4D;
-  private float[][] mTouchBorders, mA, mB;
-
-  private final int mType;
-  private final int[][][] mEnabled;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract int returnPart(int type, int face, float[] touchPoint);
-  abstract boolean isInsideFace(int face, float[] point);
-  public abstract float returnRotationFactor(int numLayers, int row);
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Movement(Static3D[] rotAxis, Static3D[] faceAxis, float[][] cuts, boolean[][] rotatable,
-           float distance3D, int size, int type, int[][][] enabled)
-    {
-    mPoint = new float[3];
-    mCamera= new float[3];
-    mTouch = new float[3];
-
-    mPoint2D = new float[2];
-    mMove2D  = new float[2];
-
-    mType = type;
-    mEnabled = enabled;
-
-    mFaceAxis   = faceAxis;
-    mNumFaceAxis= mFaceAxis.length;
-
-    mEnabledRotAxis = new int[rotAxis.length+1];
-
-    mDistanceCenterFace3D = distance3D; // distance from the center of the object to each of its faces
-
-    computeCastedAxis(rotAxis);
-    computeBorders(cuts,rotatable,size);
-    computeLinear(distance3D,rotAxis,faceAxis);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// mCastedRotAxis[1][2]{0,1} are the 2D coords of the 2nd rotAxis cast onto the face defined by the
-// 1st faceAxis.
-
-  private void computeCastedAxis(Static3D[] rotAxis)
-    {
-    mCastedRotAxis   = new float[mNumFaceAxis][rotAxis.length][2];
-    mCastedRotAxis4D = new Static4D[mNumFaceAxis][rotAxis.length];
-
-    float fx,fy,fz,f;
-
-    for( int casted=0; casted<rotAxis.length; casted++)
-      {
-      Static3D a = rotAxis[casted];
-      mPoint[0]= a.get0();
-      mPoint[1]= a.get1();
-      mPoint[2]= a.get2();
-
-      for( int face=0; face<mNumFaceAxis; face++)
-        {
-        convertTo2Dcoords( mPoint, mFaceAxis[face], mCastedRotAxis[face][casted]);
-        normalize2D(mCastedRotAxis[face][casted]);
-
-        fx = mFaceAxis[face].get0();
-        fy = mFaceAxis[face].get1();
-        fz = mFaceAxis[face].get2();
-        f  = mPoint[0]*fx + mPoint[1]*fy + mPoint[2]*fz;
-        mCastedRotAxis4D[face][casted] = new Static4D( mPoint[0]-f*fx, mPoint[1]-f*fy, mPoint[2]-f*fz, 0);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void normalize2D(float[] vect)
-    {
-    float len = (float)Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]);
-    vect[0] /= len;
-    vect[1] /= len;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// find the casted axis with which our move2D vector forms an angle closest to 90 deg.
-
-  private int computeRotationIndex(int faceAxis, float[] move2D, int[] enabled)
-    {
-    float cosAngle, minCosAngle = Float.MAX_VALUE;
-    int minIndex=0, index;
-    float m0 = move2D[0];
-    float m1 = move2D[1];
-    float len = (float)Math.sqrt(m0*m0 + m1*m1);
-
-    if( len!=0.0f )
-      {
-      m0 /= len;
-      m1 /= len;
-      }
-    else
-      {
-      m0 = 1.0f;  // arbitrarily
-      m1 = 0.0f;  //
-      }
-
-    int numAxis = enabled[0];
-
-    for(int axis=1; axis<=numAxis; axis++)
-      {
-      index = enabled[axis];
-      cosAngle = m0*mCastedRotAxis[faceAxis][index][0] + m1*mCastedRotAxis[faceAxis][index][1];
-      if( cosAngle<0 ) cosAngle = -cosAngle;
-
-      if( cosAngle<minCosAngle )
-        {
-        minCosAngle=cosAngle;
-        minIndex = index;
-        }
-      }
-
-    return minIndex;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// in the center of the face offset is always 0 regardless of the axis
-
-  private float computeOffset(float[] point, float[] axis)
-    {
-    return point[0]*axis[0] + point[1]*axis[1];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean faceIsVisible(Static3D faceAxis)
-    {
-    float castCameraOnAxis = mCamera[0]*faceAxis.get0() + mCamera[1]*faceAxis.get1() + mCamera[2]*faceAxis.get2();
-    return castCameraOnAxis > mDistanceCenterFace3D;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// given precomputed mCamera and mPoint, respectively camera and touch point positions in ScreenSpace,
-// compute point 'output[]' which:
-// 1) lies on a face of the Object, i.e. surface defined by (axis, distance from (0,0,0))
-// 2) is co-linear with mCamera and mPoint
-//
-// output = camera + alpha*(point-camera), where alpha = [dist-axis*camera] / [axis*(point-camera)]
-
-  private void castTouchPointOntoFace(Static3D faceAxis, float[] output)
-    {
-    float d0 = mPoint[0]-mCamera[0];
-    float d1 = mPoint[1]-mCamera[1];
-    float d2 = mPoint[2]-mCamera[2];
-    float a0 = faceAxis.get0();
-    float a1 = faceAxis.get1();
-    float a2 = faceAxis.get2();
-
-    float denom = a0*d0 + a1*d1 + a2*d2;
-
-    if( denom != 0.0f )
-      {
-      float axisCam = a0*mCamera[0] + a1*mCamera[1] + a2*mCamera[2];
-      float alpha = (mDistanceCenterFace3D-axisCam)/denom;
-
-      output[0] = mCamera[0] + d0*alpha;
-      output[1] = mCamera[1] + d1*alpha;
-      output[2] = mCamera[2] + d2*alpha;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Convert the 3D point3D into a 2D point on the same face surface, but in a different
-// coordinate system: a in-plane 2D coord where the origin is in the point where the axis intersects
-// the surface, and whose Y axis points 'north' i.e. is in the plane given by the 3D origin, the
-// original 3D Y axis and our 2D in-plane origin.
-// If those 3 points constitute a degenerate triangle which does not define a plane - which can only
-// happen if axis is vertical (or in theory when 2D origin and 3D origin meet, but that would have to
-// mean that the distance between the center of the Object and its faces is 0) - then we arbitrarily
-// decide that 2D Y = (0,0,-1) in the North Pole and (0,0,1) in the South Pole)
-
-  private void convertTo2Dcoords(float[] point3D, Static3D faceAxis, float[] output)
-    {
-    float y0,y1,y2; // base Y vector of the 2D coord system
-    float a0 = faceAxis.get0();
-    float a1 = faceAxis.get1();
-    float a2 = faceAxis.get2();
-
-    if( a0==0.0f && a2==0.0f )
-      {
-      y0=0; y1=0; y2=-a1;
-      }
-    else if( a1==0.0f )
-      {
-      y0=0; y1=1; y2=0;
-      }
-    else
-      {
-      float norm = (float)(-a1/Math.sqrt(1-a1*a1));
-      y0 = norm*a0; y1= norm*(a1-1/a1); y2=norm*a2;
-      }
-
-    float x0 = y1*a2 - y2*a1;  //
-    float x1 = y2*a0 - y0*a2;  // (2D coord baseY) x (axis) = 2D coord baseX
-    float x2 = y0*a1 - y1*a0;  //
-
-    float originAlpha = point3D[0]*a0 + point3D[1]*a1 + point3D[2]*a2;
-
-    float origin0 = originAlpha*a0; // coords of the point where axis
-    float origin1 = originAlpha*a1; // intersects surface plane i.e.
-    float origin2 = originAlpha*a2; // the origin of our 2D coord system
-
-    float v0 = point3D[0] - origin0;
-    float v1 = point3D[1] - origin1;
-    float v2 = point3D[2] - origin2;
-
-    output[0] = v0*x0 + v1*x1 + v2*x2;
-    output[1] = v0*y0 + v1*y1 + v2*y2;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float[] computeBorder(float[] cuts, boolean[] rotatable, int size)
-    {
-    int len = cuts.length;
-    float[] border = new float[len];
-
-    for(int i=0; i<len; i++)
-      {
-      if( !rotatable[i] )
-        {
-        border[i] = i>0 ? border[i-1] : -Float.MAX_VALUE;
-        }
-      else
-        {
-        if( rotatable[i+1] ) border[i] = cuts[i]/size;
-        else
-          {
-          int found = -1;
-
-          for(int j=i+2; j<=len; j++)
-            {
-            if( rotatable[j] )
-              {
-              found=j;
-              break;
-              }
-            }
-
-          border[i] = found>0 ? (cuts[i]+cuts[found-1])/(2*size) : Float.MAX_VALUE;
-          }
-        }
-      }
-
-    return border;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// size, not numLayers (see Master Skewb where size!=numLayers)
-
-  void computeBorders(float[][] cuts, boolean[][] rotatable, int size)
-    {
-    int numCuts = cuts.length;
-    mTouchBorders = new float[numCuts][];
-
-    for(int i=0; i<numCuts; i++)
-      {
-      mTouchBorders[i] = computeBorder(cuts[i],rotatable[i],size);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int computeSign(Static3D a, Static3D b)
-    {
-    float a1 = a.get0();
-    float a2 = a.get1();
-    float a3 = a.get2();
-    float b1 = b.get0();
-    float b2 = b.get1();
-    float b3 = b.get2();
-
-    return a1*b1+a2*b2+a3*b3 < 0 ? 1:-1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float crossProductLen(Static3D a, Static3D b)
-    {
-    float a1 = a.get0();
-    float a2 = a.get1();
-    float a3 = a.get2();
-    float b1 = b.get0();
-    float b2 = b.get1();
-    float b3 = b.get2();
-
-    float x1 = a2*b3-a3*b2;
-    float x2 = a3*b1-a1*b3;
-    float x3 = a1*b2-a2*b1;
-
-    return (float)Math.sqrt(x1*x1 + x2*x2 + x3*x3);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// compute the array of 'A' and 'B' coeffs of the Ax+B linear function by which we need to multiply
-// the 3D 'cuts' to translate it from 3D (i.e. with respect to the rotAxis) to 2D in-face (i.e. with
-// respect to the 2D rotAxis cast into a particular face)
-
-  private void computeLinear(float distance3D, Static3D[] rotAxis, Static3D[] faceAxis)
-    {
-    int numFaces = faceAxis.length;
-    int numRot   = rotAxis.length;
-
-    mA = new float[numFaces][numRot];
-    mB = new float[numFaces][numRot];
-
-    for(int i=0; i<numFaces; i++)
-      for(int j=0; j<numRot; j++)
-        {
-        mA[i][j] = crossProductLen(faceAxis[i],rotAxis[j]);
-
-        if( mA[i][j]!=0.0f )
-          {
-          float coeff = (float)Math.sqrt(1/(mA[i][j]*mA[i][j]) -1);
-          int sign = computeSign(faceAxis[i],rotAxis[j]);
-          mB[i][j] = sign*distance3D*coeff;
-          }
-        else mB[i][j] = 0.0f;
-        }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int computeRowFromOffset(int face, int axisIndex, float offset)
-    {
-    float[] borders = mTouchBorders[axisIndex];
-    int len = borders.length;
-    float A = mA[face][axisIndex];
-
-    if( A!=0.0f )
-      {
-      float B = mB[face][axisIndex];
-
-      for(int i=0; i<len; i++)
-        {
-        float translated = B + borders[i]/A;
-        if( offset<translated ) return i;
-        }
-      }
-
-    return len;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void computeEnabledAxis(int face, float[] touchPoint, int[] enabled)
-    {
-    int part = returnPart(mType,face,touchPoint);
-
-    int num = mEnabled[face][0].length;
-    enabled[0] = num;
-    System.arraycopy(mEnabled[face][part], 0, enabled, 1, num);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean faceTouched(Static4D rotatedTouchPoint, Static4D rotatedCamera, float objectRatio)
-    {
-    mPoint[0]  = rotatedTouchPoint.get0()/objectRatio;
-    mPoint[1]  = rotatedTouchPoint.get1()/objectRatio;
-    mPoint[2]  = rotatedTouchPoint.get2()/objectRatio;
-
-    mCamera[0] = rotatedCamera.get0()/objectRatio;
-    mCamera[1] = rotatedCamera.get1()/objectRatio;
-    mCamera[2] = rotatedCamera.get2()/objectRatio;
-
-    for( mLastTouchedFace=0; mLastTouchedFace<mNumFaceAxis; mLastTouchedFace++)
-      {
-      if( faceIsVisible(mFaceAxis[mLastTouchedFace]) )
-        {
-        castTouchPointOntoFace(mFaceAxis[mLastTouchedFace], mTouch);
-        convertTo2Dcoords(mTouch, mFaceAxis[mLastTouchedFace], mPoint2D);
-        if( isInsideFace(mLastTouchedFace,mPoint2D) ) return true;
-        }
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Static2D newRotation(Static4D rotatedTouchPoint, float objectRatio)
-    {
-    mPoint[0] = rotatedTouchPoint.get0()/objectRatio;
-    mPoint[1] = rotatedTouchPoint.get1()/objectRatio;
-    mPoint[2] = rotatedTouchPoint.get2()/objectRatio;
-
-    castTouchPointOntoFace(mFaceAxis[mLastTouchedFace], mTouch);
-    convertTo2Dcoords(mTouch, mFaceAxis[mLastTouchedFace], mMove2D);
-
-    mMove2D[0] -= mPoint2D[0];
-    mMove2D[1] -= mPoint2D[1];
-
-    computeEnabledAxis(mLastTouchedFace, mPoint2D, mEnabledRotAxis);
-    int rotIndex = computeRotationIndex(mLastTouchedFace, mMove2D, mEnabledRotAxis);
-    float offset = computeOffset(mPoint2D, mCastedRotAxis[mLastTouchedFace][rotIndex]);
-    int row      = computeRowFromOffset(mLastTouchedFace,rotIndex,offset);
-
-    return new Static2D(rotIndex,row);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Static4D getCastedRotAxis(int rotIndex)
-    {
-    return mCastedRotAxis4D[mLastTouchedFace][rotIndex];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getTouchedFace()
-    {
-    return mLastTouchedFace;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getTouchedPoint3D()
-    {
-    return mTouch;
-    }
-  }
diff --git a/src/main/java/org/distorted/objects/Movement12.java b/src/main/java/org/distorted/objects/Movement12.java
deleted file mode 100644
index 910fa84a..00000000
--- a/src/main/java/org/distorted/objects/Movement12.java
+++ /dev/null
@@ -1,186 +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 static org.distorted.objects.TwistyMinx.C2;
-import static org.distorted.objects.TwistyMinx.COS54;
-import static org.distorted.objects.TwistyMinx.LEN;
-import static org.distorted.objects.TwistyMinx.SIN54;
-import static org.distorted.objects.TwistyObject.SQ5;
-
-import org.distorted.library.type.Static3D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Dodecahedral objects: map the 2D swipes of user's fingers to 3D rotations
-
-class Movement12 extends Movement
-{
-  static final float DIST3D = (float)Math.sqrt(0.625f+0.275f*SQ5);
-  static final float DIST2D = (SIN54/COS54)/2;
-
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(    C2/LEN, SIN54/LEN,    0      ),
-           new Static3D(    C2/LEN,-SIN54/LEN,    0      ),
-           new Static3D(   -C2/LEN, SIN54/LEN,    0      ),
-           new Static3D(   -C2/LEN,-SIN54/LEN,    0      ),
-           new Static3D( 0        ,    C2/LEN, SIN54/LEN ),
-           new Static3D( 0        ,    C2/LEN,-SIN54/LEN ),
-           new Static3D( 0        ,   -C2/LEN, SIN54/LEN ),
-           new Static3D( 0        ,   -C2/LEN,-SIN54/LEN ),
-           new Static3D( SIN54/LEN,    0     ,    C2/LEN ),
-           new Static3D( SIN54/LEN,    0     ,   -C2/LEN ),
-           new Static3D(-SIN54/LEN,    0     ,    C2/LEN ),
-           new Static3D(-SIN54/LEN,    0     ,   -C2/LEN )
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Movement12(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
-    {
-    super(rotAxis, FACE_AXIS, cuts,rotatable,DIST3D, size, type, enabled);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(int numLayers, int row)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return angle (in radians) that the line connecting the center C of the pentagonal face and the
-// first vertex of the pentagon makes with a vertical line coming upwards from the center C.
-
-  private float returnAngle(int face)
-    {
-    switch(face)
-      {
-      case  0:
-      case  2:
-      case  6:
-      case  7: return 0.0f;
-      case  1:
-      case  3:
-      case  4:
-      case  5: return (float)(36*Math.PI/180);
-      case  9:
-      case 10: return (float)(54*Math.PI/180);
-      case  8:
-      case 11: return (float)(18*Math.PI/180);
-      }
-
-    return 0.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// The pair (distance,angle) defines a point P in R^2 in polar coordinate system. Let V be the vector
-// from the center of the coordinate system to P.
-// Let P' be the point defined by polar (distance,angle+PI/2). Let Lh be the half-line starting at
-// P' and going in the direction of V.
-// Return true iff point 'point' lies on the left of Lh, i.e. when we rotate (using the center of
-// the coordinate system as the center of rotation) 'point' and Lh in such a way that Lh points
-// directly upwards, is 'point' on the left or the right of it?
-
-  private boolean isOnTheLeft(float[] point, float distance, float angle)
-    {
-    float sin = (float)Math.sin(angle);
-    float cos = (float)Math.cos(angle);
-
-    float vx = point[0] + sin*distance;
-    float vy = point[1] - cos*distance;
-
-    return vx*sin < vy*cos;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int returnPart(int type, int face, float[] point)
-    {
-    switch(type)
-      {
-      case TYPE_SPLIT_EDGE  : return partEdge(point,face);
-      case TYPE_SPLIT_CORNER: return partCorner(point,face);
-      default               : return 0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Return 0,1,2,3,4 - the vertex of the pentagon to which point 'point' is the closest, if the
-// 'point' is inside the pentagon - or -1 otherwise.
-// The 'first' vertex is the one we meet the first when we rotate clockwise starting from 12:00.
-// This vertex makes angle 'returnAngle()' with the line coming out upwards from the center of the
-// pentagon.
-// Distance from the center to a vertex of the pentagon = 1/(6*COS54)
-
-  int partEdge(float[] point, int face)
-    {
-    float angle = returnAngle(face);
-    float A = (float)(Math.PI/5);
-
-    for(int i=0; i<5; i++)
-      {
-      if( isOnTheLeft(point, DIST2D, (9-2*i)*A-angle) ) return -1;
-      }
-
-    if( isOnTheLeft(point, 0, 2.5f*A-angle) )
-      {
-      if( isOnTheLeft(point, 0, 3.5f*A-angle) )
-        {
-        return isOnTheLeft(point, 0, 5.5f*A-angle) ? 3 : 4;
-        }
-      else return 0;
-      }
-    else
-      {
-      if( isOnTheLeft(point, 0, 4.5f*A-angle) )
-        {
-        return 2;
-        }
-      else
-        {
-        return isOnTheLeft(point, 0, 6.5f*A-angle) ? 1 : 0;
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// TODO - no such object yet
-
-  int partCorner(float[] point, int face)
-    {
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean isInsideFace(int face, float[] p)
-    {
-    float angle = returnAngle(face);
-    float A = (float)(Math.PI/5);
-
-    for(int i=0; i<5; i++)
-      {
-      if( isOnTheLeft(p, DIST2D, (9-2*i)*A-angle) ) return false;
-      }
-
-    return true;
-    }
-}
diff --git a/src/main/java/org/distorted/objects/Movement4.java b/src/main/java/org/distorted/objects/Movement4.java
deleted file mode 100644
index dbccd4ed..00000000
--- a/src/main/java/org/distorted/objects/Movement4.java
+++ /dev/null
@@ -1,102 +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 org.distorted.library.type.Static3D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Tetrahedral objects: map the 2D swipes of user's fingers to 3D rotations
-
-class Movement4 extends Movement
-{
-  static final float DIST3D = SQ6/12;
-  static final float DIST2D = SQ3/6;
-
-  static final Static3D[] FACE_AXIS = new Static3D[]
-         {
-           new Static3D(     0,+SQ3/3,+SQ6/3),
-           new Static3D(     0,+SQ3/3,-SQ6/3),
-           new Static3D(-SQ6/3,-SQ3/3,     0),
-           new Static3D(+SQ6/3,-SQ3/3,     0),
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Movement4(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
-    {
-    super(rotAxis, FACE_AXIS, cuts, rotatable, DIST3D, size, type, enabled);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// corner    edge
-//   |       \ 0 /
-// 2 | 0      \ /
-//  / \      2 | 1
-// / 1 \       |
-
-  int returnPart(int type, int face, float[] touchPoint)
-    {
-    switch(type)
-      {
-      case TYPE_NOT_SPLIT   : return 0;
-
-      case TYPE_SPLIT_EDGE  : float y1 = (face > 1 ? touchPoint[1] : -touchPoint[1]);
-                              float x1 = touchPoint[0];
-
-                              boolean e0 = x1>0;
-                              boolean e1 = y1>(+SQ3/3)*x1;
-                              boolean e2 = y1>(-SQ3/3)*x1;
-
-                              if(  e1 && e2 ) return 0;
-                              if( !e1 && e0 ) return 1;
-                              if( !e0 &&!e2 ) return 2;
-
-      case TYPE_SPLIT_CORNER: float y2 = (face > 1 ? touchPoint[1] : -touchPoint[1]);
-                              float x2 = touchPoint[0];
-
-                              boolean c0 = x2>0;
-                              boolean c1 = y2>(+SQ3/3)*x2;
-                              boolean c2 = y2>(-SQ3/3)*x2;
-
-                              if(  c0 && c2 ) return 0;
-                              if( !c1 &&!c2 ) return 1;
-                              if( !c0 && c1 ) return 2;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Jing has nL=2
-
-  public float returnRotationFactor(int numLayers, int row)
-    {
-    return numLayers==2 ? 1.0f : ((float)numLayers)/(numLayers-row);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean isInsideFace(int face, float[] p)
-    {
-    float y = (face > 1 ? p[1] : -p[1]);
-    float x = p[0];
-    return (y >= -DIST2D) && (y <= DIST2D*(2-6*x)) && (y <= DIST2D*(2+6*x));
-    }
-}
diff --git a/src/main/java/org/distorted/objects/Movement6.java b/src/main/java/org/distorted/objects/Movement6.java
deleted file mode 100644
index 4ea12b47..00000000
--- a/src/main/java/org/distorted/objects/Movement6.java
+++ /dev/null
@@ -1,82 +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 org.distorted.library.type.Static3D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Hexahedral objects: map the 2D swipes of user's fingers to 3D rotations
-
-class Movement6 extends Movement
-{
-  static final float DIST3D = 0.5f;
-  static final float DIST2D = 0.5f;
-
-  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)
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Movement6(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
-    {
-    super(rotAxis, FACE_AXIS, cuts, rotatable, DIST3D, size, type, enabled);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-//  corner    edge
-//  \ 0 /     3 | 0
-// 3 \ / 1  ___ | ___
-//   / \        |
-//  / 2 \     2 | 1
-
-  int returnPart(int type, int face, float[] touchPoint)
-    {
-    switch(type)
-      {
-      case TYPE_NOT_SPLIT   : return 0;
-      case TYPE_SPLIT_EDGE  : boolean e0 = touchPoint[0] > 0;
-                              boolean e1 = touchPoint[1] > 0;
-                              return e0 ? (e1 ? 0:1) : (e1 ? 3:2);
-      case TYPE_SPLIT_CORNER: boolean c0 = touchPoint[1] >= touchPoint[0];
-                              boolean c1 = touchPoint[1] >=-touchPoint[0];
-                              return c0 ? (c1 ? 0:3) : (c1 ? 1:2);
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(int numLayers, int row)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean isInsideFace(int face, float[] p)
-    {
-    return ( p[0]<=DIST2D && p[0]>=-DIST2D && p[1]<=DIST2D && p[1]>=-DIST2D );
-    }
-}
diff --git a/src/main/java/org/distorted/objects/Movement8.java b/src/main/java/org/distorted/objects/Movement8.java
deleted file mode 100644
index 32afaa60..00000000
--- a/src/main/java/org/distorted/objects/Movement8.java
+++ /dev/null
@@ -1,101 +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 org.distorted.library.type.Static3D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Octahedral objects: map the 2D swipes of user's fingers to 3D rotations
-
-class Movement8 extends Movement
-{
-  static final float DIST3D = SQ6/6;
-  static final float DIST2D = SQ3/6;
-
-  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)
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Movement8(Static3D[] rotAxis,float[][] cuts, boolean[][] rotatable, int size, int type, int[][][] enabled)
-    {
-    super(rotAxis, FACE_AXIS, cuts, rotatable, DIST3D, size, type, enabled);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// corner    edge
-//   |       \ 0 /
-// 2 | 0      \ /
-//  / \      2 | 1
-// / 1 \       |
-
-  int returnPart(int type, int face, float[] touchPoint)
-    {
-    switch(type)
-      {
-      case TYPE_NOT_SPLIT   : return 0;
-
-      case TYPE_SPLIT_EDGE  : float y1 = (face%2 == 0 ? touchPoint[1] : -touchPoint[1]);
-                              float x1 = touchPoint[0];
-
-                              boolean e0 = x1>0;
-                              boolean e1 = y1>(+SQ3/3)*x1;
-                              boolean e2 = y1>(-SQ3/3)*x1;
-
-                              if(  e1 && e2 ) return 0;
-                              if( !e1 && e0 ) return 1;
-                              if( !e0 &&!e2 ) return 2;
-
-      case TYPE_SPLIT_CORNER: float y2 = (face%2 == 0 ? touchPoint[1] : -touchPoint[1]);
-                              float x2 = touchPoint[0];
-
-                              boolean c0 = x2>0;
-                              boolean c1 = y2>(+SQ3/3)*x2;
-                              boolean c2 = y2>(-SQ3/3)*x2;
-
-                              if(  c0 && c2 ) return 0;
-                              if( !c1 &&!c2 ) return 1;
-                              if( !c0 && c1 ) return 2;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float returnRotationFactor(int numLayers, int row)
-    {
-    return 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean isInsideFace(int face, float[] p)
-    {
-    float y = (face%2 == 0 ? p[1] : -p[1]);
-    float x = p[0];
-    return (y >= -DIST2D) && (y <= DIST2D*(2-6*x)) && (y <= DIST2D*(2+6*x));
-    }
-}
diff --git a/src/main/java/org/distorted/objects/ObjectList.java b/src/main/java/org/distorted/objects/ObjectList.java
deleted file mode 100644
index 186e585a..00000000
--- a/src/main/java/org/distorted/objects/ObjectList.java
+++ /dev/null
@@ -1,566 +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;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public enum ObjectList
-  {
-  ///////////////////// Size // DB Level // NumScrambles // Mesh // small icon // medium icon // big icon // huge icon
-
-  CUBE (
-         new int[][] {
-                       {2 , 12, 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, 17, 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, 24, 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, 28, R.raw.cube5, R.drawable.ui_small_cube5, R.drawable.ui_medium_cube5, R.drawable.ui_big_cube5, R.drawable.ui_huge_cube5}
-                     },
-         0
-       ),
-
-  JING (
-         new int[][] {
-                       {2 , 11, 11, R.raw.jing, R.drawable.ui_small_jing2, R.drawable.ui_medium_jing2, R.drawable.ui_big_jing2, R.drawable.ui_huge_jing2} ,
-                     },
-         1
-       ),
-
-  PYRA (
-         new int[][] {
-                       {3 , 10, 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, 17, 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, 23, R.raw.pyra5, R.drawable.ui_small_pyra5, R.drawable.ui_medium_pyra5, R.drawable.ui_big_pyra5, R.drawable.ui_huge_pyra5}
-                     },
-         1
-       ),
-
-  KILO (
-         new int[][] {
-                       {3 , 18, 18, R.raw.kilo3, R.drawable.ui_small_kilo3, R.drawable.ui_medium_kilo3, R.drawable.ui_big_kilo3, R.drawable.ui_huge_kilo3} ,
-                       {5 , 33, 33, R.raw.kilo5, R.drawable.ui_small_kilo5, R.drawable.ui_medium_kilo5, R.drawable.ui_big_kilo5, R.drawable.ui_huge_kilo5} ,
-                     },
-         2
-       ),
-
-  MEGA (
-         new int[][] {
-                       {3 , 21, 21, R.raw.mega3, R.drawable.ui_small_mega3, R.drawable.ui_medium_mega3, R.drawable.ui_big_mega3, R.drawable.ui_huge_mega3} ,
-                       {5 , 35, 37, R.raw.mega5, R.drawable.ui_small_mega5, R.drawable.ui_medium_mega5, R.drawable.ui_big_mega5, R.drawable.ui_huge_mega5} ,
-                     },
-         2
-       ),
-
-  ULTI (
-         new int[][] {
-                       {2 , 18, 18, R.raw.ulti, R.drawable.ui_small_ulti, R.drawable.ui_medium_ulti, R.drawable.ui_big_ulti, R.drawable.ui_huge_ulti} ,
-                     },
-         3
-       ),
-
-  DIAM (
-         new int[][] {
-                       {2 , 10, 12, R.raw.diam2, R.drawable.ui_small_diam2, R.drawable.ui_medium_diam2, R.drawable.ui_big_diam2, R.drawable.ui_huge_diam2} ,
-                       {3 , 18, 24, R.raw.diam3, R.drawable.ui_small_diam3, R.drawable.ui_medium_diam3, R.drawable.ui_big_diam3, R.drawable.ui_huge_diam3} ,
-                       {4 , 32, 32, R.raw.diam4, R.drawable.ui_small_diam4, R.drawable.ui_medium_diam4, R.drawable.ui_big_diam4, R.drawable.ui_huge_diam4} ,
-                     },
-         3
-       ),
-
-  DINO (
-         new int[][] {
-                       {3 , 10, 10, R.raw.dino, R.drawable.ui_small_dino, R.drawable.ui_medium_dino, R.drawable.ui_big_dino, R.drawable.ui_huge_dino} ,
-                     },
-         4
-       ),
-
-  DIN4 (
-         new int[][] {
-                       {3 , 7, 7, R.raw.dino, R.drawable.ui_small_din4, R.drawable.ui_medium_din4, R.drawable.ui_big_din4, R.drawable.ui_huge_din4} ,
-                     },
-         4
-       ),
-
-  REDI (
-         new int[][] {
-                       {3 , 14, 16, R.raw.redi, R.drawable.ui_small_redi, R.drawable.ui_medium_redi, R.drawable.ui_big_redi, R.drawable.ui_huge_redi} ,
-                     },
-         4
-       ),
-
-  HELI (
-         new int[][] {
-                       {3 , 18, 20, R.raw.heli, R.drawable.ui_small_heli, R.drawable.ui_medium_heli, R.drawable.ui_big_heli, R.drawable.ui_huge_heli} ,
-                     },
-         4
-       ),
-
-  SKEW (
-         new int[][] {
-                       {2 , 11, 11, R.raw.skew2, R.drawable.ui_small_skewb, R.drawable.ui_medium_skewb, R.drawable.ui_big_skewb, R.drawable.ui_huge_skewb} ,
-                       {3 , 17, 21, R.raw.skew3, R.drawable.ui_small_skewm, R.drawable.ui_medium_skewm, R.drawable.ui_big_skewm, R.drawable.ui_huge_skewm} ,
-                     },
-         5
-       ),
-
-  IVY  (
-         new int[][] {
-                       {2 , 8, 8, R.raw.ivy, R.drawable.ui_small_ivy, R.drawable.ui_medium_ivy, R.drawable.ui_big_ivy, R.drawable.ui_huge_ivy} ,
-                     },
-         5
-       ),
-
-  REX  (
-         new int[][] {
-                       {3 , 16, 19, R.raw.rex, R.drawable.ui_small_rex, R.drawable.ui_medium_rex, R.drawable.ui_big_rex, R.drawable.ui_huge_rex} ,
-                     },
-         5
-       ),
-
-  BAN1 (
-         new int[][] {
-                       {3 , 16, 16, R.raw.ban1, R.drawable.ui_small_ban1, R.drawable.ui_medium_ban1, R.drawable.ui_big_ban1, R.drawable.ui_huge_ban1} ,
-                     },
-         6
-       ),
-
-  BAN2 (
-         new int[][] {
-                       {3 , 16, 16, R.raw.ban2, R.drawable.ui_small_ban2, R.drawable.ui_medium_ban2, R.drawable.ui_big_ban2, R.drawable.ui_huge_ban2} ,
-                     },
-         6
-       ),
-
-  BAN3 (
-         new int[][] {
-                       {3 , 16, 16, R.raw.ban3, R.drawable.ui_small_ban3, R.drawable.ui_medium_ban3, R.drawable.ui_big_ban3, R.drawable.ui_huge_ban3} ,
-                     },
-         6
-       ),
-
-  BAN4 (
-         new int[][] {
-                       {3 , 16, 16, R.raw.ban4, R.drawable.ui_small_ban4, R.drawable.ui_medium_ban4, R.drawable.ui_big_ban4, R.drawable.ui_huge_ban4} ,
-                     },
-         6
-       ),
-
-  SQU1 (
-         new int[][] {
-                       {3 , 24, 24, R.raw.square1, R.drawable.ui_small_square1, R.drawable.ui_medium_square1, R.drawable.ui_big_square1, R.drawable.ui_huge_square1} ,
-                     },
-         7
-       ),
-
-  SQU2 (
-         new int[][] {
-                       {3 , 24, 24, R.raw.square2, R.drawable.ui_small_square2, R.drawable.ui_medium_square2, R.drawable.ui_big_square2, R.drawable.ui_huge_square2} ,
-                     },
-         7
-       ),
-
-  MIRR (
-         new int[][] {
-                       {2 , 12, 12, R.raw.mirr2, R.drawable.ui_small_mirr2, R.drawable.ui_medium_mirr2, R.drawable.ui_big_mirr2, R.drawable.ui_huge_mirr2} ,
-                       {3 , 16, 17, R.raw.mirr3, R.drawable.ui_small_mirr3, R.drawable.ui_medium_mirr3, R.drawable.ui_big_mirr3, R.drawable.ui_huge_mirr3} ,
-                     },
-         7
-       ),
-  ;
-
-  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_SCRAMBLE;
-  public static final int MAX_OBJECT_SIZE;
-
-  private final int[] mObjectSizes, mDBLevels, mNumScrambles, mSmallIconIDs, mMediumIconIDs, mBigIconIDs, mHugeIconIDs, mResourceIDs;
-  private final int mRow, 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 maxScramble= 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.mNumScrambles[j]> maxScramble ) maxScramble= object.mNumScrambles[j];
-        if( object.mDBLevels[j]    > maxLevel    ) maxLevel   = object.mDBLevels[j];
-        if( object.mObjectSizes[j] > maxSize     ) maxSize    = object.mObjectSizes[j];
-        }
-      }
-
-    MAX_NUM_OBJECTS = maxNum;
-    MAX_LEVEL       = maxLevel;
-    MAX_SCRAMBLE    = maxScramble;
-    MAX_OBJECT_SIZE = maxSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void setUpColAndRow()
-    {
-    mIndices = new int[NUM_OBJECTS];
-    mRowCount= 0;
-
-    for(int obj=0; obj<NUM_OBJECTS; obj++)
-      {
-      mIndices[obj] = objects[obj].mRow;
-      if( mIndices[obj]>=mRowCount ) mRowCount = mIndices[obj]+1;
-      }
-
-    mColCount = 0;
-
-    for(int row=0; row<mRowCount; row++)
-      {
-      int numObjects = computeNumObjectsInRow(row);
-      if( numObjects>mColCount ) mColCount = numObjects;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int computeNumObjectsInRow(int row)
-    {
-    int num=0;
-
-    for(int object=0; object<NUM_OBJECTS; object++)
-      {
-      if( objects[object].mRow == row )
-        {
-        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 getDBLevel(int ordinal, int sizeIndex)
-    {
-    if( ordinal>=0 && ordinal<NUM_OBJECTS )
-      {
-      int num = objects[ordinal].mObjectSizes.length;
-      return sizeIndex>=0 && sizeIndex<num ? objects[ordinal].mDBLevels[sizeIndex] : 0;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getNumScramble(int ordinal, int sizeIndex)
-    {
-    if( ordinal>=0 && ordinal<NUM_OBJECTS )
-      {
-      int num = objects[ordinal].mObjectSizes.length;
-      return sizeIndex>=0 && sizeIndex<num ? objects[ordinal].mNumScrambles[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;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  ObjectList(int[][] info, int row)
-    {
-    mNumSizes = info.length;
-
-    mObjectSizes  = new int[mNumSizes];
-    mDBLevels     = new int[mNumSizes];
-    mNumScrambles = 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];
-      mDBLevels[i]     = info[i][1];
-      mNumScrambles[i] = info[i][2];
-      mResourceIDs[i]  = info[i][3];
-      mSmallIconIDs[i] = info[i][4];
-      mMediumIconIDs[i]= info[i][5];
-      mBigIconIDs[i]   = info[i][6];
-      mHugeIconIDs[i]  = info[i][7];
-      }
-
-    mRow  = row;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getSizes()
-    {
-    return mObjectSizes;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 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 TwistyJing          (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  2: return new TwistyPyraminx      (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  3: return new TwistyKilominx      (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  4: return new TwistyMegaminx      (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  5: return new TwistyUltimate      (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  6: return new TwistyDiamond       (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  7: return new TwistyDino6         (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  8: return new TwistyDino4         (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case  9: return new TwistyRedi          (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 10: return new TwistyHelicopter    (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 11: return new TwistySkewb         (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 12: return new TwistyIvy           (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 13: return new TwistyRex           (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 14: return new TwistyBandagedFused (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 15: return new TwistyBandaged2Bar  (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 16: return new TwistyBandaged3Plate(size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 17: return new TwistyBandagedEvil  (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 18: return new TwistySquare1       (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 19: return new TwistySquare2       (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 20: return new TwistyMirror        (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      }
-
-    return null;
-    }
-  }
diff --git a/src/main/java/org/distorted/objects/Twisty12.java b/src/main/java/org/distorted/objects/Twisty12.java
deleted file mode 100644
index ee6f0fd0..00000000
--- a/src/main/java/org/distorted/objects/Twisty12.java
+++ /dev/null
@@ -1,95 +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 org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class Twisty12 extends TwistyObject
-{
-  static final int MINX_LGREEN = 0xff53aa00;
-  static final int MINX_PINK   = 0xfffd7ab7;
-  static final int MINX_SANDY  = 0xffefd48b;
-  static final int MINX_LBLUE  = 0xff00a2d7;
-  static final int MINX_ORANGE = 0xffff6200;
-  static final int MINX_VIOLET = 0xff7d59a4;
-  static final int MINX_DGREEN = 0xff007a47;
-  static final int MINX_DRED   = 0xffbd0000;
-  static final int MINX_DBLUE  = 0xff1a29b2;
-  static final int MINX_DYELLOW= 0xffffc400;
-  static final int MINX_WHITE  = 0xffffffff;
-  static final int MINX_GREY   = 0xff727c7b;
-
-  static final int[] FACE_COLORS = new int[]
-         {
-           MINX_LGREEN, MINX_PINK   , MINX_SANDY , MINX_LBLUE,
-           MINX_ORANGE, MINX_VIOLET , MINX_DGREEN, MINX_DRED ,
-           MINX_DBLUE , MINX_DYELLOW, MINX_WHITE , MINX_GREY
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Twisty12(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-           DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
-    {
-    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getColor(int face)
-    {
-    return FACE_COLORS[face];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaceColors()
-    {
-    return 12;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFOV()
-    {
-    return 30;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.35f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return 1.0f;
-    }
-}
diff --git a/src/main/java/org/distorted/objects/Twisty4.java b/src/main/java/org/distorted/objects/Twisty4.java
deleted file mode 100644
index 54ddeb82..00000000
--- a/src/main/java/org/distorted/objects/Twisty4.java
+++ /dev/null
@@ -1,81 +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 org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class Twisty4 extends TwistyObject
-{
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_GREEN , COLOR_YELLOW,
-           COLOR_BLUE  , COLOR_RED
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Twisty4(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-          DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
-    {
-    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getColor(int face)
-    {
-    return FACE_COLORS[face];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaceColors()
-    {
-    return 4;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFOV()
-    {
-    return 30;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.88f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return getNumLayers()/(SQ6/3);
-    }
-}
diff --git a/src/main/java/org/distorted/objects/Twisty6.java b/src/main/java/org/distorted/objects/Twisty6.java
deleted file mode 100644
index 074922f6..00000000
--- a/src/main/java/org/distorted/objects/Twisty6.java
+++ /dev/null
@@ -1,82 +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 org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class Twisty6 extends TwistyObject
-{
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_YELLOW, COLOR_WHITE,
-           COLOR_BLUE  , COLOR_GREEN,
-           COLOR_RED   , COLOR_ORANGE
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Twisty6(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-          DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
-    {
-    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getColor(int face)
-    {
-    return FACE_COLORS[face];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaceColors()
-    {
-    return 6;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFOV()
-    {
-    return 60;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.5f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return getNumLayers();
-    }
-}
diff --git a/src/main/java/org/distorted/objects/Twisty8.java b/src/main/java/org/distorted/objects/Twisty8.java
deleted file mode 100644
index e9e020e3..00000000
--- a/src/main/java/org/distorted/objects/Twisty8.java
+++ /dev/null
@@ -1,83 +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 org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshSquare;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class Twisty8 extends TwistyObject
-{
-  private static final int[] FACE_COLORS = new int[]
-         {
-           COLOR_ORANGE, COLOR_VIOLET,
-           COLOR_WHITE , COLOR_BLUE  ,
-           COLOR_YELLOW, COLOR_RED   ,
-           COLOR_GREEN , COLOR_GREY
-         };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  Twisty8(int numLayers, int realSize, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-          DistortedEffects effects, int[][] moves, ObjectList list, Resources res, int scrWidth)
-    {
-    super(numLayers, realSize, quat, texture, mesh, effects, moves, list, res, scrWidth);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getColor(int face)
-    {
-    return FACE_COLORS[face];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getNumFaceColors()
-    {
-    return 8;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getFOV()
-    {
-    return 60;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float getScreenRatio()
-    {
-    return 0.65f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float returnMultiplier()
-    {
-    return 1.5f;
-    }
-}
diff --git a/src/main/java/org/distorted/objects/TwistyBandaged2Bar.java b/src/main/java/org/distorted/objects/TwistyBandaged2Bar.java
index b1189d75..54e3f3e0 100644
--- a/src/main/java/org/distorted/objects/TwistyBandaged2Bar.java
+++ b/src/main/java/org/distorted/objects/TwistyBandaged2Bar.java
@@ -21,26 +21,27 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyBandaged2Bar extends TwistyBandagedAbstract
+public class TwistyBandaged2Bar extends TwistyBandagedAbstract
 {
-  TwistyBandaged2Bar(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                     DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyBandaged2Bar(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                            DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, quat, texture, mesh, effects, moves, ObjectList.BAN2, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java b/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java
index e7b6fb35..1e7a5172 100644
--- a/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java
+++ b/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java
@@ -21,26 +21,27 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
+import org.distorted.objectlb.ScrambleState;
 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.helpers.ScrambleState;
 import org.distorted.main.R;
+import org.distorted.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyBandaged3Plate extends TwistyBandagedAbstract
+public class TwistyBandaged3Plate extends TwistyBandagedAbstract
 {
-  TwistyBandaged3Plate(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                       DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyBandaged3Plate(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                              DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, quat, texture, mesh, effects, moves, ObjectList.BAN3, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyBandagedAbstract.java b/src/main/java/org/distorted/objects/TwistyBandagedAbstract.java
index e7381c74..6991d6d2 100644
--- a/src/main/java/org/distorted/objects/TwistyBandagedAbstract.java
+++ b/src/main/java/org/distorted/objects/TwistyBandagedAbstract.java
@@ -19,18 +19,22 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -123,7 +127,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -156,7 +160,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -220,7 +224,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mInitQuats ==null )
       {
@@ -240,14 +244,14 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return mDimensions.length;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     float[][] pos = getPositions();
 
@@ -262,7 +266,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
@@ -297,7 +301,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int size)
+  protected float[][] getCubitPositions(int size)
     {
     int numCubits = getNumCubits();
     float[][] tmp = new float[numCubits][];
@@ -312,7 +316,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -320,7 +324,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( numLayers<2 ) return null;
 
@@ -356,21 +360,21 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return NUM_STICKERS;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
@@ -405,7 +409,7 @@ abstract class TwistyBandagedAbstract extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( mFaceMap==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyBandagedEvil.java b/src/main/java/org/distorted/objects/TwistyBandagedEvil.java
index 7365c1e7..dc538d61 100644
--- a/src/main/java/org/distorted/objects/TwistyBandagedEvil.java
+++ b/src/main/java/org/distorted/objects/TwistyBandagedEvil.java
@@ -21,26 +21,27 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyBandagedEvil extends TwistyBandagedAbstract
+public class TwistyBandagedEvil extends TwistyBandagedAbstract
 {
-  TwistyBandagedEvil(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                     DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyBandagedEvil(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                            DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, quat, texture, mesh, effects, moves, ObjectList.BAN4, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyBandagedFused.java b/src/main/java/org/distorted/objects/TwistyBandagedFused.java
index b699efd7..651c270f 100644
--- a/src/main/java/org/distorted/objects/TwistyBandagedFused.java
+++ b/src/main/java/org/distorted/objects/TwistyBandagedFused.java
@@ -21,26 +21,27 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyBandagedFused extends TwistyBandagedAbstract
+public class TwistyBandagedFused extends TwistyBandagedAbstract
 {
-  TwistyBandagedFused(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                      DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyBandagedFused(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                             DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, quat, texture, mesh, effects, moves, ObjectList.BAN1, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyCube.java b/src/main/java/org/distorted/objects/TwistyCube.java
index f1b6557b..b1e39051 100644
--- a/src/main/java/org/distorted/objects/TwistyCube.java
+++ b/src/main/java/org/distorted/objects/TwistyCube.java
@@ -19,23 +19,27 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyCube extends Twisty6
+public class TwistyCube extends Twisty6
 {
   static final Static3D[] ROT_AXIS = new Static3D[]
          {
@@ -59,15 +63,15 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyCube(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-             DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyCube(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                    DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.CUBE, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -158,7 +162,7 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats ==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -167,7 +171,7 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int extraI, extraV, num;
     float height;
@@ -214,7 +218,7 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats ==null ) initializeQuats();
     return mQuats[0];
@@ -222,21 +226,21 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
@@ -253,7 +257,7 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     int numCubits = numLayers>1 ? 6*numLayers*numLayers - 12*numLayers + 8 : 1;
     float[][] tmp = new float[numCubits][];
@@ -274,7 +278,7 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats ==null ) initializeQuats();
     return mQuats;
@@ -282,7 +286,7 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( numLayers<2 ) return null;
 
@@ -318,30 +322,30 @@ class TwistyCube extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
-    return CUBITS[cubit].mRotationRow[cubitface/2] == (cubitface%2==0 ? (1<<(numLayers-1)):1) ? cubitface : NUM_TEXTURES;
+    return CUBITS[cubit].getRotRow(cubitface/2) == (cubitface%2==0 ? (1<<(numLayers-1)):1) ? cubitface : NUM_TEXTURES;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objects/TwistyDiamond.java b/src/main/java/org/distorted/objects/TwistyDiamond.java
index 2bc947d2..aaa207e5 100644
--- a/src/main/java/org/distorted/objects/TwistyDiamond.java
+++ b/src/main/java/org/distorted/objects/TwistyDiamond.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement8;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty8;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -63,15 +67,15 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyDiamond(int size, Static4D quat, DistortedTexture texture,
-                MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyDiamond(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                       DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.DIAM, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -118,7 +122,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     if( mFaceMap==null ) mFaceMap = new int[] {4,0,6,2,7,3,5,1};
@@ -128,7 +132,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -136,21 +140,21 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( numLayers<2 ) return null;
 
@@ -188,7 +192,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 8;
     }
@@ -302,7 +306,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int layers)
+  protected float[][] getCubitPositions(int layers)
     {
     int numO = getNumOctahedrons(layers);
     int numT = getNumTetrahedrons(layers);
@@ -349,7 +353,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
     int N = numLayers>3 ? 5:6;
@@ -403,7 +407,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int numO = getNumOctahedrons(numLayers);
@@ -427,21 +431,21 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<getNumOctahedrons(numLayers) ? 0 : 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     int numO = getNumOctahedrons(size);
 
@@ -462,7 +466,7 @@ public class TwistyDiamond extends Twisty8
         case 7: axis = 0; layer =             1; break;
         }
 
-      return CUBITS[cubit].mRotationRow[axis] == layer ? cubitface : NUM_TEXTURES;
+      return CUBITS[cubit].getRotRow(axis) == layer ? cubitface : NUM_TEXTURES;
       }
     else
       {
@@ -472,7 +476,7 @@ public class TwistyDiamond extends Twisty8
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyDino.java b/src/main/java/org/distorted/objects/TwistyDino.java
index 17896bc3..eb0a44ea 100644
--- a/src/main/java/org/distorted/objects/TwistyDino.java
+++ b/src/main/java/org/distorted/objects/TwistyDino.java
@@ -19,22 +19,26 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_CORNER;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_CORNER;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public abstract class TwistyDino extends Twisty6
+abstract class TwistyDino extends Twisty6
 {
   // the four rotation axis of a RubikDino. Must be normalized.
   static final Static3D[] ROT_AXIS = new Static3D[]
@@ -95,7 +99,7 @@ public abstract class TwistyDino extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -103,7 +107,7 @@ public abstract class TwistyDino extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int size)
+  protected float[][] getCuts(int size)
     {
     if( mCuts==null )
       {
@@ -129,21 +133,21 @@ public abstract class TwistyDino extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 4;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int size)
+  protected float[][] getCubitPositions(int size)
     {
     if( mCenters ==null )
       {
@@ -169,7 +173,7 @@ public abstract class TwistyDino extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     double[][] vertices = new double[][] { {-1.5, 0.0, 0.0},{ 1.5, 0.0, 0.0},{ 0.0,-1.5, 0.0},{ 0.0, 0.0,-1.5} };
     int[][] vert_indices= new int[][] { {2,1,0},{3,0,1},{2,3,1},{3,2,0} };
@@ -184,7 +188,7 @@ public abstract class TwistyDino extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     return mQuats[cubit];
@@ -192,21 +196,21 @@ public abstract class TwistyDino extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyDino4.java b/src/main/java/org/distorted/objects/TwistyDino4.java
index 243b6889..4bbe1042 100644
--- a/src/main/java/org/distorted/objects/TwistyDino4.java
+++ b/src/main/java/org/distorted/objects/TwistyDino4.java
@@ -21,12 +21,13 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -36,15 +37,15 @@ public class TwistyDino4 extends TwistyDino
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyDino4(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-              DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public 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);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -67,14 +68,14 @@ public class TwistyDino4 extends TwistyDino
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     return null;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( mFaceMap==null ) mFaceMap = new int[] { 4, 2, 2, 4, 0, 2, 1, 4, 0, 0, 1, 1 };
     return (cubitface==0 || cubitface==1) ? mFaceMap[cubit] : NUM_TEXTURES;
@@ -82,7 +83,7 @@ public class TwistyDino4 extends TwistyDino
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 1;
     }
diff --git a/src/main/java/org/distorted/objects/TwistyDino6.java b/src/main/java/org/distorted/objects/TwistyDino6.java
index 76da2944..ca8ca65d 100644
--- a/src/main/java/org/distorted/objects/TwistyDino6.java
+++ b/src/main/java/org/distorted/objects/TwistyDino6.java
@@ -21,12 +21,13 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -36,15 +37,15 @@ public class TwistyDino6 extends TwistyDino
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyDino6(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-              DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public 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);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -67,14 +68,14 @@ public class TwistyDino6 extends TwistyDino
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     return null;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( mFaceMap==null )
       {
@@ -91,7 +92,7 @@ public class TwistyDino6 extends TwistyDino
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 2;
     }
diff --git a/src/main/java/org/distorted/objects/TwistyHelicopter.java b/src/main/java/org/distorted/objects/TwistyHelicopter.java
index 894c7134..6330d839 100644
--- a/src/main/java/org/distorted/objects/TwistyHelicopter.java
+++ b/src/main/java/org/distorted/objects/TwistyHelicopter.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_EDGE;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_EDGE;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -71,15 +75,15 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyHelicopter(int size, Static4D quat, DistortedTexture texture,
-                   MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyHelicopter(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                          DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.HELI, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -141,7 +145,7 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -150,7 +154,7 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -158,21 +162,21 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int size)
+  protected float[][] getCuts(int size)
     {
     if( mCuts==null )
       {
@@ -198,14 +202,14 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 4;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int size)
+  protected float[][] getCubitPositions(int size)
     {
     if( mCenters==null )
       {
@@ -261,7 +265,7 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -324,7 +328,7 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     if( mQuatIndices==null ) mQuatIndices = new int[] { 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 };
@@ -333,21 +337,21 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<8 ? 0:1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( mFaceMap==null )
       {
@@ -399,7 +403,7 @@ public class TwistyHelicopter extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyIvy.java b/src/main/java/org/distorted/objects/TwistyIvy.java
index 8c828cda..c73f32f4 100644
--- a/src/main/java/org/distorted/objects/TwistyIvy.java
+++ b/src/main/java/org/distorted/objects/TwistyIvy.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_CORNER;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_CORNER;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -70,15 +74,15 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyIvy(int size, Static4D quat, DistortedTexture texture,
-            MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyIvy(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                   DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.IVY, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -117,7 +121,7 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -126,7 +130,7 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -134,21 +138,21 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return NUM_STICKERS;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
@@ -175,14 +179,14 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     final float DIST_CORNER = numLayers-1;
     final float DIST_CENTER = numLayers-1;
@@ -205,7 +209,7 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -362,7 +366,7 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
 
@@ -386,21 +390,21 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<4 ? 0:1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( mFaceMap==null )
       {
@@ -425,7 +429,7 @@ public class TwistyIvy extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyJing.java b/src/main/java/org/distorted/objects/TwistyJing.java
index 8c4e07b1..6615bb73 100644
--- a/src/main/java/org/distorted/objects/TwistyJing.java
+++ b/src/main/java/org/distorted/objects/TwistyJing.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement4;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty4;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -67,15 +71,15 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyJing(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-             DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyJing(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                    DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.JING, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -113,7 +117,7 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -122,7 +126,7 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int size)
+  protected float[][] getCubitPositions(int size)
     {
     if( mCenters==null )
       {
@@ -152,7 +156,7 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -160,21 +164,21 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int size)
+  protected float[][] getCuts(int size)
     {
     if( mCuts==null )
       {
@@ -201,14 +205,14 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( mFaceMap==null )
       {
@@ -242,7 +246,7 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -350,7 +354,7 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     if( mRotQuat ==null ) mRotQuat = new int[] {0,1,2,7,0,2,7,6,3,10,0,1,3,5};
@@ -359,21 +363,21 @@ public class TwistyJing extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<4 ? 0 : (cubit<10?1:2);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyKilominx.java b/src/main/java/org/distorted/objects/TwistyKilominx.java
index 9115f915..79fdb58d 100644
--- a/src/main/java/org/distorted/objects/TwistyKilominx.java
+++ b/src/main/java/org/distorted/objects/TwistyKilominx.java
@@ -19,16 +19,20 @@
 
 package org.distorted.objects;
 
+import static org.distorted.objectlb.Movement12.COS54;
+import static org.distorted.objectlb.Movement12.SIN54;
+
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.QuatHelper;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.QuatHelper;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -38,8 +42,8 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyKilominx(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                 DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyKilominx(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                        DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.KILO, res, scrWidth);
     }
@@ -81,14 +85,14 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return numLayers<5 ? 1 : numLayers/2 + 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     return genericGetCuts(numLayers,0.5f);
     }
@@ -227,7 +231,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     if( mCorners==null ) initializeCorners();
     if( numLayers<5 ) return mCorners;
@@ -305,7 +309,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
     int numVariants = getNumCubitVariants(numLayers);
@@ -483,7 +487,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
@@ -494,7 +498,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     int[] sizes = ObjectList.KILO.getSizes();
     int variants = sizes.length;
@@ -505,7 +509,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
 
@@ -601,7 +605,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
     int numCubitsPerEdge   = numCubitsPerEdge(numLayers);
@@ -624,7 +628,7 @@ public class TwistyKilominx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyMegaminx.java b/src/main/java/org/distorted/objects/TwistyMegaminx.java
index 77e36d44..667cc598 100644
--- a/src/main/java/org/distorted/objects/TwistyMegaminx.java
+++ b/src/main/java/org/distorted/objects/TwistyMegaminx.java
@@ -19,16 +19,20 @@
 
 package org.distorted.objects;
 
+import static org.distorted.objectlb.Movement12.COS54;
+import static org.distorted.objectlb.Movement12.SIN54;
+
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.QuatHelper;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.QuatHelper;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -39,8 +43,8 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyMegaminx(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                 DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyMegaminx(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                        DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.MEGA, res, scrWidth);
     }
@@ -68,14 +72,14 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return (numLayers+3)/2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     return genericGetCuts(numLayers,0.5f-MEGA_D);
     }
@@ -189,7 +193,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
     int numCubitsPerEdge   = numCubitsPerEdge(numLayers);
@@ -249,7 +253,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
     int numVariants = getNumCubitVariants(numLayers);
@@ -399,7 +403,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
@@ -410,7 +414,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     int[] sizes = ObjectList.MEGA.getSizes();
     int variants = sizes.length;
@@ -420,7 +424,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
 
@@ -506,7 +510,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     int numCubitsPerCorner = numCubitsPerCorner(numLayers);
     int numCubitsPerEdge   = numCubitsPerEdge(numLayers);
@@ -529,7 +533,7 @@ public class TwistyMegaminx extends TwistyMinx
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyMinx.java b/src/main/java/org/distorted/objects/TwistyMinx.java
index c92b8db1..5325aed7 100644
--- a/src/main/java/org/distorted/objects/TwistyMinx.java
+++ b/src/main/java/org/distorted/objects/TwistyMinx.java
@@ -19,17 +19,24 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_EDGE;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_EDGE;
+import static org.distorted.objectlb.Movement12.C2;
+import static org.distorted.objectlb.Movement12.LEN;
+import static org.distorted.objectlb.Movement12.SIN54;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement12;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty12;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -39,10 +46,6 @@ abstract class TwistyMinx extends Twisty12
   static final int NUM_CENTERS = 12;
   static final int NUM_EDGES   = 30;
 
-  static final float C2       = (SQ5+3)/4;
-  static final float LEN      = (float)(Math.sqrt(1.25f+0.5f*SQ5));
-  static final float SIN54    = (SQ5+1)/4;
-  static final float COS54    = (float)(Math.sqrt(10-2*SQ5)/4);
   static final float SIN18    = (SQ5-1)/4;
   static final float COS18    = (float)(0.25f*Math.sqrt(10.0f+2.0f*SQ5));
   static final float COS_HALFD= (float)(Math.sqrt(0.5f-0.1f*SQ5)); // cos(half the dihedral angle)
@@ -102,7 +105,7 @@ abstract class TwistyMinx extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -509,7 +512,7 @@ abstract class TwistyMinx extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     if( mFaceMap==null ) mFaceMap = new int[] {8,10,3,7,1,11,9,2,4,0,5,6};
@@ -519,7 +522,7 @@ abstract class TwistyMinx extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -578,14 +581,14 @@ abstract class TwistyMinx extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
diff --git a/src/main/java/org/distorted/objects/TwistyMirror.java b/src/main/java/org/distorted/objects/TwistyMirror.java
index f60918f0..f798d6c8 100644
--- a/src/main/java/org/distorted/objects/TwistyMirror.java
+++ b/src/main/java/org/distorted/objects/TwistyMirror.java
@@ -19,23 +19,27 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyMirror extends Twisty6
+public class TwistyMirror extends Twisty6
 {
   static final Static3D[] ROT_AXIS = new Static3D[]
          {
@@ -65,15 +69,15 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyMirror(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-               DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyMirror(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                      DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.MIRR, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -166,7 +170,7 @@ class TwistyMirror extends Twisty6
 // we cannot do this the standard, automatic way because there's only 1 color in the FACE_COLORS
 // table and retCubitSolvedStatus() always returns -1,-1 or 0.
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( numLayers==3 )
       {
@@ -186,7 +190,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( numLayers==2 )
       {
@@ -436,7 +440,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
@@ -458,7 +462,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return numLayers==2 ? 12 : 25;
     }
@@ -472,7 +476,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int extraI, extraV, num;
     float height;
@@ -530,7 +534,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats ==null ) initializeQuats();
     return mQuats[0];
@@ -538,14 +542,14 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 6*numLayers*numLayers - 12*numLayers + 8;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit;
     }
@@ -559,7 +563,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     if( mPositions==null )
       {
@@ -583,7 +587,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats ==null ) initializeQuats();
     return mQuats;
@@ -598,7 +602,7 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
@@ -632,14 +636,14 @@ class TwistyMirror extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
diff --git a/src/main/java/org/distorted/objects/TwistyObject.java b/src/main/java/org/distorted/objects/TwistyObject.java
deleted file mode 100644
index d9579679..00000000
--- a/src/main/java/org/distorted/objects/TwistyObject.java
+++ /dev/null
@@ -1,1318 +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.helpers.FactoryCubit;
-import org.distorted.helpers.FactorySticker;
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.QuatHelper;
-import org.distorted.helpers.ScrambleState;
-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.DistortedLibrary;
-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 org.distorted.main.BuildConfig;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Random;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class TwistyObject extends DistortedNode
-  {
-  public static final int COLOR_YELLOW = 0xffffff00;
-  public static final int COLOR_WHITE  = 0xffffffff;
-  public static final int COLOR_BLUE   = 0xff0000ff;
-  public static final int COLOR_GREEN  = 0xff00bb00;
-  public static final int COLOR_RED    = 0xff990000;
-  public static final int COLOR_ORANGE = 0xffff6200;
-  public static final int COLOR_GREY   = 0xff727c7b;
-  public static final int COLOR_VIOLET = 0xff7700bb;
-  public static final int COLOR_BLACK  = 0xff000000;
-
-  public static final int TEXTURE_HEIGHT = 256;
-  static final int NUM_STICKERS_IN_ROW = 4;
-
-  static final float SQ2 = (float)Math.sqrt(2);
-  static final float SQ3 = (float)Math.sqrt(3);
-  static final float SQ5 = (float)Math.sqrt(5);
-  static final float SQ6 = (float)Math.sqrt(6);
-
-  private static final float NODE_RATIO = 1.60f;
-  private static final float MAX_SIZE_CHANGE = 1.35f;
-  private static final float MIN_SIZE_CHANGE = 0.75f;
-
-  private static final Static3D CENTER = new Static3D(0,0,0);
-  private static final int POST_ROTATION_MILLISEC = 500;
-
-  MeshBase[] mMeshes;
-  final Static4D[] OBJECT_QUATS;
-  final Cubit[] CUBITS;
-  final int NUM_FACE_COLORS;
-  final int NUM_TEXTURES;
-  final int NUM_CUBITS;
-  final int NUM_AXIS;
-  final int NUM_QUATS;
-
-  private final int mNumCubitFaces;
-  private final Static3D[] mAxis;
-  private final float[][] mCuts;
-  private final int[] mNumCuts;
-  private final int mNodeSize;
-  private final float[][] mOrigPos;
-  private final Static3D mNodeScale;
-  private final Static4D mQuat;
-  private final int mNumLayers, mRealSize;
-  private final ObjectList mList;
-  private final DistortedEffects mEffects;
-  private final VertexEffectRotate mRotateEffect;
-  private final Dynamic1D mRotationAngle;
-  private final Static3D mRotationAxis;
-  private final Static3D mObjectScale;
-  private final int[] mQuatDebug;
-  private final float mCameraDist;
-  private final Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
-  private final DistortedTexture mTexture;
-  private final float mInitScreenRatio;
-  private final int mSolvedFunctionIndex;
-  private final boolean mIsBandaged;
-  private float mObjectScreenRatio;
-  private int[][] mSolvedQuats;
-  private int[][] mQuatMult;
-  private int[] mTmpQuats;
-  private int mNumTexRows, mNumTexCols;
-  private int mRotRowBitmap;
-  private int mRotAxis;
-  private MeshBase mMesh;
-  private final TwistyObjectScrambler mScrambler;
-
-  //////////////////// SOLVED1 ////////////////////////
-
-  private int[] mFaceMap;
-  private int[][] mScramble;
-  private int[] mColors;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  TwistyObject(int numLayers, int realSize, 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));
-
-    mNumLayers = numLayers;
-    mRealSize = realSize;
-    mList = list;
-    mOrigPos = getCubitPositions(mNumLayers);
-    mAxis = getRotationAxis();
-    mInitScreenRatio = getScreenRatio();
-    mObjectScreenRatio = 1.0f;
-    mNumCubitFaces = getNumCubitFaces();
-    mSolvedFunctionIndex = getSolvedFunctionIndex();
-
-    mCuts = getCuts(mNumLayers);
-    mNumCuts = new int[mAxis.length];
-    if( mCuts==null ) for(int i=0; i<mAxis.length; i++) mNumCuts[i] = 0;
-    else              for(int i=0; i<mAxis.length; i++) mNumCuts[i] = mCuts[i].length;
-
-    OBJECT_QUATS = getQuats();
-    NUM_CUBITS  = mOrigPos.length;
-    NUM_FACE_COLORS = getNumFaceColors();
-    NUM_TEXTURES = getNumStickerTypes(mNumLayers)*NUM_FACE_COLORS;
-    NUM_AXIS = mAxis.length;
-    NUM_QUATS = OBJECT_QUATS.length;
-
-    int scramblingType = getScrambleType();
-    ScrambleState[] states = getScrambleStates();
-    mScrambler = new TwistyObjectScrambler(scramblingType,NUM_AXIS,numLayers,states);
-
-    boolean bandaged=false;
-
-    for(int c=0; c<NUM_CUBITS; c++)
-      {
-      if( mOrigPos[c].length>3 )
-        {
-        bandaged=true;
-        break;
-        }
-      }
-
-    mIsBandaged = bandaged;
-
-    mQuatDebug = new int[NUM_CUBITS];
-
-    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
-    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
-
-    mNodeScale= new Static3D(1,NODE_RATIO,1);
-    mQuat = quat;
-
-    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/mRealSize;
-    mObjectScale = new Static3D(scale,scale,scale);
-    MatrixEffectScale scaleEffect = new MatrixEffectScale(mObjectScale);
-    MatrixEffectQuaternion quatEffect  = new MatrixEffectQuaternion(quat, CENTER);
-
-    MatrixEffectScale nodeScaleEffect = new MatrixEffectScale(mNodeScale);
-    nodeEffects.apply(nodeScaleEffect);
-
-    mNumTexCols = NUM_STICKERS_IN_ROW;
-    mNumTexRows = (NUM_TEXTURES+1)/NUM_STICKERS_IN_ROW;
-
-    if( mNumTexCols*mNumTexRows < NUM_TEXTURES+1 ) mNumTexRows++;
-
-    CUBITS = new Cubit[NUM_CUBITS];
-    createMeshAndCubits(list,res);
-    createDataStructuresForSolved(numLayers);
-
-    mTexture = new DistortedTexture();
-    mEffects = new DistortedEffects();
-
-    for(int q=0; q<NUM_QUATS; q++)
-      {
-      VertexEffectQuaternion vq = new VertexEffectQuaternion(OBJECT_QUATS[q],CENTER);
-      vq.setMeshAssociation(0,q);
-      mEffects.apply(vq);
-      }
-
-    mEffects.apply(mRotateEffect);
-    mEffects.apply(quatEffect);
-    mEffects.apply(scaleEffect);
-
-    // 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);
-
-    float fov = getFOV();
-    double halfFOV = fov * (Math.PI/360);
-    mCameraDist = 0.5f*NODE_RATIO / (float)Math.tan(halfFOV);
-
-    setProjection( fov, 0.1f);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Static3D getPos(float[] origPos)
-    {
-    int len = origPos.length/3;
-    float sumX = 0.0f;
-    float sumY = 0.0f;
-    float sumZ = 0.0f;
-
-    for(int i=0; i<len; i++)
-      {
-      sumX += origPos[3*i  ];
-      sumY += origPos[3*i+1];
-      sumZ += origPos[3*i+2];
-      }
-
-    sumX /= len;
-    sumY /= len;
-    sumZ /= len;
-
-    return new Static3D(sumX,sumY,sumZ);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createMeshAndCubits(ObjectList list, Resources res)
-    {
-    int sizeIndex = ObjectList.getSizeIndex(list.ordinal(),mNumLayers);
-    int resourceID= list.getResourceIDs()[sizeIndex];
-
-    if( resourceID!=0 )
-      {
-      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], NUM_AXIS);
-        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], NUM_AXIS);
-        cubitMesh[i] = createCubitMesh(i,mNumLayers);
-        Static3D pos = getPos(mOrigPos[i]);
-        cubitMesh[i].apply(new MatrixEffectMove(pos),1,0);
-        cubitMesh[i].setEffectAssociation(0, CUBITS[i].computeAssociation(), 0);
-        }
-
-      mMesh = new MeshJoined(cubitMesh);
-      resetAllTextureMaps();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private MeshBase createCubitMesh(int cubit, int numLayers)
-    {
-    int variant = getCubitVariant(cubit,numLayers);
-
-    if( mMeshes==null )
-      {
-      FactoryCubit factory = FactoryCubit.getInstance();
-      factory.clear();
-      mMeshes = new MeshBase[getNumCubitVariants(numLayers)];
-      }
-
-    if( mMeshes[variant]==null )
-      {
-      ObjectShape shape = getObjectShape(cubit,numLayers);
-      FactoryCubit factory = FactoryCubit.getInstance();
-      factory.createNewFaceTransform(shape);
-      mMeshes[variant] = factory.createRoundedSolid(shape);
-      }
-
-    MeshBase mesh = mMeshes[variant].copy(true);
-    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( getQuat(cubit,numLayers), new Static3D(0,0,0) );
-    mesh.apply(quat,0xffffffff,0);
-
-    return mesh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createDataStructuresForSolved(int numLayers)
-    {
-    mTmpQuats = new int[NUM_QUATS];
-    mSolvedQuats = new int[NUM_CUBITS][];
-
-    for(int c=0; c<NUM_CUBITS; c++)
-      {
-      mSolvedQuats[c] = getSolvedQuats(c,numLayers);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// This is used to build internal data structures for the generic 'isSolved()'
-//
-// if this is an internal cubit (all faces black): return -1
-// if this is a face cubit (one non-black face): return the color index of the only non-black face.
-// Color index, i.e. the index into the 'FACE_COLORS' table.
-// else (edge or corner cubit, more than one non-black face): return -2.
-
-  int retCubitSolvedStatus(int cubit, int numLayers)
-    {
-    int numNonBlack=0, nonBlackIndex=-1, color;
-
-    for(int face=0; face<mNumCubitFaces; face++)
-      {
-      color = getFaceColor(cubit,face,numLayers);
-
-      if( color<NUM_TEXTURES )
-        {
-        numNonBlack++;
-        nonBlackIndex = color%NUM_FACE_COLORS;
-        }
-      }
-
-    if( numNonBlack==0 ) return -1;
-    if( numNonBlack>=2 ) return -2;
-
-    return nonBlackIndex;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean shouldResetTextureMaps()
-    {
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int[] buildSolvedQuats(Static3D faceAx, Static4D[] quats)
-    {
-    final float MAXD = 0.0001f;
-    float x = faceAx.get0();
-    float y = faceAx.get1();
-    float z = faceAx.get2();
-    float a,dx,dy,dz,qx,qy,qz;
-    Static4D quat;
-
-    int len = quats.length;
-    int place = 0;
-
-    for(int q=1; q<len; q++)
-      {
-      quat = quats[q];
-      qx = quat.get0();
-      qy = quat.get1();
-      qz = quat.get2();
-
-           if( x!=0.0f ) { a = qx/x; }
-      else if( y!=0.0f ) { a = qy/y; }
-      else               { a = qz/z; }
-
-      dx = a*x-qx;
-      dy = a*y-qy;
-      dz = a*z-qz;
-
-      if( dx>-MAXD && dx<MAXD && dy>-MAXD && dy<MAXD && dz>-MAXD && dz<MAXD )
-        {
-        mTmpQuats[place++] = q;
-        }
-      }
-
-    if( place!=0 )
-      {
-      int[] ret = new int[place];
-      System.arraycopy(mTmpQuats,0,ret,0,place);
-      return ret;
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getMultQuat(int index1, int index2)
-    {
-    if( mQuatMult==null )
-      {
-      mQuatMult = new int[NUM_QUATS][NUM_QUATS];
-
-      for(int i=0; i<NUM_QUATS; i++)
-        for(int j=0; j<NUM_QUATS; j++) mQuatMult[i][j] = -1;
-      }
-
-    if( mQuatMult[index1][index2]==-1 )
-      {
-      mQuatMult[index1][index2] = mulQuat(index1,index2);
-      }
-
-    return mQuatMult[index1][index2];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean isSolved()
-    {
-    if( mSolvedFunctionIndex==0 ) return isSolved0();
-    if( mSolvedFunctionIndex==1 ) return isSolved1();
-    if( mSolvedFunctionIndex==2 ) return isSolved2();
-    if( mSolvedFunctionIndex==3 ) return isSolved3();
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean isSolved0()
-    {
-    int len, q1,q = CUBITS[0].mQuatIndex;
-    int[] solved;
-    boolean skip;
-
-    for(int c=1; c<NUM_CUBITS; c++)
-      {
-      q1 = CUBITS[c].mQuatIndex;
-
-      if( q1==q ) continue;
-
-      skip = false;
-      solved = mSolvedQuats[c];
-      len = solved==null ? 0:solved.length;
-
-      for(int i=0; i<len; i++)
-        {
-        if( q1==getMultQuat(q,solved[i]) )
-          {
-          skip = true;
-          break;
-          }
-        }
-
-      if( !skip ) return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int computeScramble(int quatNum, int centerNum)
-    {
-    float MAXDIFF = 0.01f;
-    float[] center= mOrigPos[centerNum];
-    Static4D sc = new Static4D(center[0], center[1], center[2], 1.0f);
-    Static4D result = QuatHelper.rotateVectorByQuat(sc,OBJECT_QUATS[quatNum]);
-
-    float x = result.get0();
-    float y = result.get1();
-    float z = result.get2();
-
-    for(int c=0; c<NUM_CUBITS; c++)
-      {
-      float[] cent = mOrigPos[c];
-
-      float qx = cent[0] - x;
-      float qy = cent[1] - y;
-      float qz = cent[2] - z;
-
-      if( qx>-MAXDIFF && qx<MAXDIFF &&
-          qy>-MAXDIFF && qy<MAXDIFF &&
-          qz>-MAXDIFF && qz<MAXDIFF  ) return c;
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Dino4 uses this. It is solved if and only if groups of cubits
-// (0,3,7), (1,2,5), (4,8,9), (6,10,11)
-// or
-// (0,1,4), (2,3,6), (5,9,10), (7,8,11)
-// are all the same color.
-
-  public boolean isSolved1()
-    {
-    if( mScramble==null )
-      {
-      mScramble = new int[NUM_QUATS][NUM_CUBITS];
-      mColors   = new int[NUM_CUBITS];
-
-      for(int q=0; q<NUM_QUATS; q++)
-        for(int c=0; c<NUM_CUBITS; c++) mScramble[q][c] = computeScramble(q,c);
-      }
-
-    if( mFaceMap==null )
-      {
-      mFaceMap = new int[] { 4, 2, 2, 4, 0, 2, 1, 4, 0, 0, 1, 1 };
-      }
-
-    for(int c=0; c<NUM_CUBITS; c++)
-      {
-      int index = mScramble[CUBITS[c].mQuatIndex][c];
-      mColors[index] = mFaceMap[c];
-      }
-
-    if( mColors[0]==mColors[3] && mColors[0]==mColors[7] &&
-        mColors[1]==mColors[2] && mColors[1]==mColors[5] &&
-        mColors[4]==mColors[8] && mColors[4]==mColors[9]  ) return true;
-
-    if( mColors[0]==mColors[1] && mColors[0]==mColors[4] &&
-        mColors[2]==mColors[3] && mColors[2]==mColors[6] &&
-        mColors[5]==mColors[9] && mColors[5]==mColors[10] ) return true;
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Dino6 uses this. It 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 isSolved2()
-    {
-    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) );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Square-2 is solved iff
-// a) all of its cubits are rotated with the same quat
-// b) its two 'middle' cubits are rotated with the same quat, the 6 'front' and 6 'back'
-// edges and corners with this quat multiplied by QUATS[18] (i.e. those are upside down)
-// and all the 12 left and right edges and corners also with the same quat multiplied by
-// QUATS[12] - i.e. also upside down.
-
-  public boolean isSolved3()
-    {
-    int index = CUBITS[0].mQuatIndex;
-
-    if( CUBITS[1].mQuatIndex!=index ) return false;
-
-    boolean solved = true;
-
-    for(int i=2; i<NUM_CUBITS; i++)
-      {
-      if( CUBITS[i].mQuatIndex!=index )
-        {
-        solved = false;
-        break;
-        }
-      }
-
-    if( solved ) return true;
-
-    int indexX = mulQuat(index,12);  // QUATS[12] = 180deg (1,0,0)
-    int indexZ = mulQuat(index,18);  // QUATS[18] = 180deg (0,0,1)
-
-    for(int i= 2; i<        18; i+=2) if( CUBITS[i].mQuatIndex != indexZ ) return false;
-    for(int i= 3; i<        18; i+=2) if( CUBITS[i].mQuatIndex != indexX ) return false;
-    for(int i=18; i<NUM_CUBITS; i+=2) if( CUBITS[i].mQuatIndex != indexX ) return false;
-    for(int i=19; i<NUM_CUBITS; i+=2) if( CUBITS[i].mQuatIndex != indexZ ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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/mRealSize;
-    mObjectScale.set(scale,scale,scale);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float getObjectRatio()
-    {
-    return mObjectScreenRatio*mInitScreenRatio;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int computeRow(float[] pos, int axisIndex)
-    {
-    int ret=0;
-    int len = pos.length / 3;
-    Static3D axis = mAxis[axisIndex];
-    float axisX = axis.get0();
-    float axisY = axis.get1();
-    float axisZ = axis.get2();
-    float casted;
-
-    for(int i=0; i<len; i++)
-      {
-      casted = pos[3*i]*axisX + pos[3*i+1]*axisY + pos[3*i+2]*axisZ;
-      ret |= computeSingleRow(axisIndex,casted);
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int computeSingleRow(int axisIndex,float casted)
-    {
-    int num = mNumCuts[axisIndex];
-
-    for(int i=0; i<num; i++)
-      {
-      if( casted<mCuts[axisIndex][i] ) return (1<<i);
-      }
-
-    return (1<<num);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean wasRotateApplied()
-    {
-    return mEffects.exists(mRotateEffect.getID());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean belongsToRotation( int cubit, int axis, int rowBitmap)
-    {
-    return (CUBITS[cubit].mRotationRow[axis] & 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 = mAxis[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[] basic = getBasicAngle();
-
-      for(int[] move: moves)
-        {
-        axis     = move[0];
-        rowBitmap= move[1];
-        angle    = move[2]*(360/basic[axis]);
-        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);
-            }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getScrambleType()
-    {
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int computeBitmapFromRow(int rowBitmap, int axis)
-    {
-    if( mIsBandaged )
-      {
-      int bitmap, initBitmap=0;
-
-      while( initBitmap!=rowBitmap )
-        {
-        initBitmap = rowBitmap;
-
-        for(int cubit=0; cubit<NUM_CUBITS; cubit++)
-          {
-          bitmap = CUBITS[cubit].mRotationRow[axis];
-          if( (rowBitmap & bitmap) != 0 ) rowBitmap |= bitmap;
-          }
-        }
-      }
-
-    return rowBitmap;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Clamp all rotated positions to one of those original ones to avoid accumulating errors.
-// Do so only if minimal Error is appropriately low (shape-shifting puzzles - Square-1)
-
-  void clampPos(float[] pos, int offset)
-    {
-    float currError, minError = Float.MAX_VALUE;
-    int minErrorIndex1 = -1;
-    int minErrorIndex2 = -1;
-
-    float x = pos[offset  ];
-    float y = pos[offset+1];
-    float z = pos[offset+2];
-
-    float xo,yo,zo;
-
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      int len = mOrigPos[i].length / 3;
-
-      for(int j=0; j<len; j++)
-        {
-        xo = mOrigPos[i][3*j  ];
-        yo = mOrigPos[i][3*j+1];
-        zo = mOrigPos[i][3*j+2];
-
-        currError = (xo-x)*(xo-x) + (yo-y)*(yo-y) + (zo-z)*(zo-z);
-
-        if( currError<minError )
-          {
-          minError = currError;
-          minErrorIndex1 = i;
-          minErrorIndex2 = j;
-          }
-        }
-      }
-
-    if( minError< 0.1f ) // TODO: 0.1 ?
-      {
-      pos[offset  ] = mOrigPos[minErrorIndex1][3*minErrorIndex2  ];
-      pos[offset+1] = mOrigPos[minErrorIndex1][3*minErrorIndex2+1];
-      pos[offset+2] = mOrigPos[minErrorIndex1][3*minErrorIndex2+2];
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// remember about the double cover or unit quaternions!
-
-  int mulQuat(int q1, int q2)
-    {
-    Static4D result = QuatHelper.quatMultiply(OBJECT_QUATS[q1],OBJECT_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<NUM_QUATS; i++)
-      {
-      dX = OBJECT_QUATS[i].get0() - rX;
-      dY = OBJECT_QUATS[i].get1() - rY;
-      dZ = OBJECT_QUATS[i].get2() - rZ;
-      dW = OBJECT_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 = OBJECT_QUATS[i].get0() + rX;
-      dY = OBJECT_QUATS[i].get1() + rY;
-      dZ = OBJECT_QUATS[i].get2() + rZ;
-      dW = OBJECT_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;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCubitFaceColorIndex(int cubit, int face)
-    {
-    Static4D texMap = mMesh.getTextureMap(NUM_FACE_COLORS*cubit + face);
-
-    int x = (int)(texMap.get0()/texMap.get2());
-    int y = (int)(texMap.get1()/texMap.get3());
-
-    return (mNumTexRows-1-y)*NUM_STICKERS_IN_ROW + x;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// the getFaceColors + final black in a grid (so that we do not exceed the maximum texture size)
-
-  public void createTexture()
-    {
-    Bitmap bitmap;
-
-    Paint paint = new Paint();
-    bitmap = Bitmap.createBitmap( mNumTexCols*TEXTURE_HEIGHT, mNumTexRows*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(COLOR_BLACK);
-    canvas.drawRect(0, 0, mNumTexCols*TEXTURE_HEIGHT, mNumTexRows*TEXTURE_HEIGHT, paint);
-
-    int face = 0;
-    FactorySticker factory = FactorySticker.getInstance();
-
-    for(int row=0; row<mNumTexRows; row++)
-      for(int col=0; col<mNumTexCols; col++)
-        {
-        if( face>=NUM_TEXTURES ) break;
-        ObjectSticker sticker = retSticker(face);
-        factory.drawRoundedPolygon(canvas, paint, col*TEXTURE_HEIGHT, row*TEXTURE_HEIGHT, getColor(face%NUM_FACE_COLORS), sticker);
-        face++;
-        }
-
-    if( !mTexture.setTexture(bitmap) )
-      {
-      int max = DistortedLibrary.getMaxTextureSize();
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.log("failed to set texture of size "+bitmap.getWidth()+"x"+bitmap.getHeight()+" max is "+max);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumLayers()
-    {
-    return mNumLayers;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void continueRotation(float angleInDegrees)
-    {
-    mRotationAngleStatic.set0(angleInDegrees);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Static4D getRotationQuat()
-      {
-      return mQuat;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void recomputeScaleFactor(int scrWidth)
-    {
-    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)
-    {
-    boolean error = false;
-
-    for(int i=0; i<NUM_CUBITS; i++)
-      {
-      mQuatDebug[i] = CUBITS[i].restorePreferences(preferences);
-
-      if( mQuatDebug[i]>=0 && mQuatDebug[i]<NUM_QUATS)
-        {
-        CUBITS[i].modifyCurrentPosition(OBJECT_QUATS[mQuatDebug[i]]);
-        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),mQuatDebug[i]);
-        }
-      else
-        {
-        error = true;
-        }
-      }
-
-    if( error )
-      {
-      for(int i=0; i<NUM_CUBITS; i++)
-        {
-        CUBITS[i].solve();
-        mMesh.setEffectAssociation(i, CUBITS[i].computeAssociation(),0);
-        }
-      recordQuatsState("Failed to restorePreferences");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void recordQuatsState(String message)
-    {
-    StringBuilder quats = new StringBuilder();
-
-    for(int j=0; j<NUM_CUBITS; j++)
-      {
-      quats.append(mQuatDebug[j]);
-      quats.append(" ");
-      }
-
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.e("quats" , quats.toString());
-      android.util.Log.e("object", mList.name()+"_"+mNumLayers);
-      }
-    else
-      {
-      Exception ex = new Exception(message);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("quats" , quats.toString());
-      crashlytics.setCustomKey("object", mList.name()+"_"+mNumLayers );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void releaseResources()
-    {
-    mTexture.markForDeletion();
-    mMesh.markForDeletion();
-    mEffects.markForDeletion();
-
-    for(int j=0; j<NUM_CUBITS; j++)
-      {
-      CUBITS[j].releaseResources();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 ratioW = 1.0f/mNumTexCols;
-    final float ratioH = 1.0f/mNumTexRows;
-    int color, row, col;
-
-    for(int cubit=0; cubit<NUM_CUBITS; cubit++)
-      {
-      final Static4D[] maps = new Static4D[mNumCubitFaces];
-
-      for(int cubitface=0; cubitface<mNumCubitFaces; cubitface++)
-        {
-        color = getFaceColor(cubit,cubitface,mNumLayers);
-        row = (mNumTexRows-1) - color/mNumTexCols;
-        col = color%mNumTexCols;
-        maps[cubitface] = new Static4D( col*ratioW, row*ratioH, ratioW, ratioH);
-        }
-
-      mMesh.setTextureMap(maps,mNumCubitFaces*cubit);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setTextureMap(int cubit, int face, int newColor)
-    {
-    final float ratioW = 1.0f/mNumTexCols;
-    final float ratioH = 1.0f/mNumTexRows;
-    final Static4D[] maps = new Static4D[mNumCubitFaces];
-    int row = (mNumTexRows-1) - newColor/mNumTexCols;
-    int col = newColor%mNumTexCols;
-
-    maps[face] = new Static4D( col*ratioW, row*ratioH, ratioW, ratioH);
-    mMesh.setTextureMap(maps,mNumCubitFaces*cubit);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void beginNewRotation(int axis, int row )
-    {
-    if( axis<0 || axis>=NUM_AXIS )
-      {
-      android.util.Log.e("object", "invalid rotation axis: "+axis);
-      return;
-      }
-    if( row<0 || row>=mNumLayers )
-      {
-      android.util.Log.e("object", "invalid rotation row: "+row);
-      return;
-      }
-
-    mRotAxis     = axis;
-    mRotRowBitmap= computeBitmapFromRow( (1<<row),axis );
-    mRotationAngleStatic.set0(0.0f);
-    mRotationAxis.set( mAxis[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 )
-    {
-    if( wasRotateApplied() )
-      {
-      mRotAxis     = axis;
-      mRotRowBitmap= computeBitmapFromRow( rowBitmap,axis );
-
-      mRotationAngleStatic.set0(0.0f);
-      mRotationAxis.set( mAxis[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();
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public long finishRotationNow(EffectListener listener, int nearestAngleInDegrees)
-    {
-    if( wasRotateApplied() )
-      {
-      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();
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 = mAxis[mRotAxis].get0();
-    float axisY = mAxis[mRotAxis].get1();
-    float axisZ = mAxis[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(int axis, float angle, float speed)
-    {
-    int[] basicArray = getBasicAngle();
-    int basicAngle   = basicArray[axis>=basicArray.length ? 0 : axis];
-    int nearestAngle = 360/basicAngle;
-
-    int tmp = (int)((angle+nearestAngle/2)/nearestAngle);
-    if( angle< -(nearestAngle*0.5) ) tmp-=1;
-
-    if( tmp!=0 ) return nearestAngle*tmp;
-
-    return speed> 1.2f ? nearestAngle*(angle>0 ? 1:-1) : 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float getCameraDist()
-    {
-    return mCameraDist;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNodeSize()
-    {
-    return mNodeSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ObjectList getObjectList()
-    {
-    return mList;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void randomizeNewScramble(int[][] scramble, Random rnd, int curr, int total)
-    {
-    mScrambler.randomizeNewScramble(scramble,rnd,curr,total);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract float getScreenRatio();
-  abstract float[][] getCubitPositions(int numLayers);
-  abstract Static4D[] getQuats();
-  abstract int getNumFaceColors();
-  abstract int getNumStickerTypes(int numLayers);
-  abstract int getNumCubitFaces();
-  abstract ObjectSticker retSticker(int face);
-  abstract int getColor(int face);
-  abstract int getFaceColor(int cubit, int cubitface, int numLayers);
-  abstract float returnMultiplier();
-  abstract float[][] getCuts(int numLayers);
-  abstract int getCubitVariant(int cubit, int numLayers);
-  abstract int getNumCubitVariants(int numLayers);
-  abstract Static4D getQuat(int cubit, int numLayers);
-  abstract ObjectShape getObjectShape(int cubit, int numLayers);
-  abstract int[] getSolvedQuats(int cubit, int numLayers);
-  abstract int getSolvedFunctionIndex();
-  abstract int getFOV();
-  abstract ScrambleState[] getScrambleStates();
-
-  public abstract Movement getMovement();
-  public abstract Static3D[] getRotationAxis();
-  public abstract int[] getBasicAngle();
-  public abstract int getObjectName(int numLayers);
-  public abstract int getInventor(int numLayers);
-  public abstract int getComplexity(int numLayers);
-  }
diff --git a/src/main/java/org/distorted/objects/TwistyObjectScrambler.java b/src/main/java/org/distorted/objects/TwistyObjectScrambler.java
deleted file mode 100644
index 844e3d2a..00000000
--- a/src/main/java/org/distorted/objects/TwistyObjectScrambler.java
+++ /dev/null
@@ -1,394 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.objects;
-
-import org.distorted.helpers.ScrambleState;
-
-import java.util.Random;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TwistyObjectScrambler
-  {
-  private final ScrambleState[] mStates;
-  private final int mType;
-  private final int mNumAxis;
-  private final int mNumLayers;
-
-  // type=0, i.e. main
-  private int mCurrState;
-  private int mIndexExcluded;
-  private int[][] mScrambleTable;
-  private int[] mNumOccurences;
-
-  // type=1, i.e. the exception: Square-1
-  private static final int BASIC_ANGLE = 12;
-  private static final int LAST_SL = 0; // automatic rotations: last rot was a 'slash' i.e. along ROT_AXIS[1]
-  private static final int LAST_UP = 1; // last rot was along ROT_AXIS[0], upper layer and forelast was a slash
-  private static final int LAST_LO = 2; // last rot was along ROT_AXIS[0], lower layer and forelast was a slash
-  private static final int LAST_UL = 3; // two last rots were along ROT_AXIS[0] (so the next must be a slash)
-
-  private int[][] mPermittedAngles;
-  private int[] mCornerQuat;
-  private int mPermittedUp, mPermittedDo;
-  private int[][] mBadCornerQuats;
-  private int mLastRot;
-  private int[][] mQuatMult;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  TwistyObjectScrambler(int type, int numAxis, int numLayers, ScrambleState[] states)
-    {
-    mType = type;
-    mNumAxis = numAxis;
-    mNumLayers = numLayers;
-    mStates = states;
-
-    if( mType==1 )
-      {
-      mPermittedAngles = new int[2][BASIC_ANGLE];
-      mCornerQuat = new int[8];
-      mLastRot = LAST_SL;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// QUATS[i]*QUATS[j] = QUATS[QUAT_MULT[i][j]]
-
-  void initializeQuatMult()
-    {
-    mQuatMult = new int[][]
-      {
-        {  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,},
-        {  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12,},
-        {  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13,},
-        {  3,  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14,},
-        {  4,  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3, 16, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15,},
-        {  5,  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4, 17, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16,},
-        {  6,  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5, 18, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17,},
-        {  7,  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6, 19, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18,},
-        {  8,  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7, 20, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19,},
-        {  9, 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8, 21, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20,},
-        { 10, 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 22, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,},
-        { 11,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,},
-        { 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,},
-        { 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,},
-        { 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16, 15,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,  3,},
-        { 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17, 16,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,  4,},
-        { 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18, 17,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,  5,},
-        { 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19, 18,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,  6,},
-        { 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20, 19,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,  7,},
-        { 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21, 20,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,  8,},
-        { 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22, 21,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,  9,},
-        { 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 22,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11, 10,},
-        { 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 23, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0, 11,},
-        { 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0,}
-      };
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void initializeScrambling()
-    {
-    if( mScrambleTable ==null )
-      {
-      mScrambleTable = new int[mNumAxis][mNumLayers];
-      }
-    if( mNumOccurences ==null )
-      {
-      int max=0;
-
-      for (ScrambleState mState : mStates)
-        {
-        int tmp = mState.getTotal(-1);
-        if (max < tmp) max = tmp;
-        }
-
-      mNumOccurences = new int[max];
-      }
-
-    for(int i=0; i<mNumAxis; i++)
-      for(int j=0; j<mNumLayers; j++) mScrambleTable[i][j] = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  private void randomizeNewScramble0(int[][] scramble, Random rnd, int curr, int total)
-    {
-    if( curr==0 )
-      {
-      mCurrState     = 0;
-      mIndexExcluded =-1;
-      initializeScrambling();
-      }
-
-    int[] info= mStates[mCurrState].getRandom(rnd, mIndexExcluded, mScrambleTable, mNumOccurences);
-
-    scramble[curr][0] = info[0];
-    scramble[curr][1] = info[1];
-    scramble[curr][2] = info[2];
-
-    mCurrState     = info[3];
-    mIndexExcluded = info[0];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean cornerIsUp(int index)
-    {
-    return ((index<4) ^ (mCornerQuat[index]>=12));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean cornerIsLeft(int index)
-    {
-    int q = mCornerQuat[index];
-
-    switch(index)
-      {
-      case 0:
-      case 4: return ((q>=3 && q<= 7) || (q>=18 && q<=22));
-      case 1:
-      case 5: return ((q>=6 && q<=10) || (q>=15 && q<=19));
-      case 2:
-      case 6: return ((q==0 || q==1 || (q>=9 && q<=11)) || (q>=12 && q<=16));
-      case 3:
-      case 7: return ((q>=0 && q<=4) || (q==12 || q==13 || (q>=21 && q<=23)));
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean quatIsBad(int quatIndex, int corner)
-    {
-    if( mBadCornerQuats ==null )
-      {
-      // quat indices that make corner cubits bandage the puzzle.
-      mBadCornerQuats = new int[][] { { 2, 8,17,23}, { 5,11,14,20} };
-      }
-
-    int index = (corner%2);
-
-    return ( quatIndex== mBadCornerQuats[index][0] ||
-             quatIndex== mBadCornerQuats[index][1] ||
-             quatIndex== mBadCornerQuats[index][2] ||
-             quatIndex== mBadCornerQuats[index][3]  );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean isPermittedDo(int angle)
-    {
-    if( mQuatMult==null ) initializeQuatMult();
-
-    for(int corner=0; corner<8; corner++)
-      {
-      if( !cornerIsUp(corner) )
-        {
-        int currQuat = mCornerQuat[corner];
-        int finalQuat= mQuatMult[angle][currQuat];
-        if( quatIsBad(finalQuat,corner) ) return false;
-        }
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean isPermittedUp(int angle)
-    {
-    if( mQuatMult==null ) initializeQuatMult();
-
-    for(int corner=0; corner<8; corner++)
-      {
-      if( cornerIsUp(corner) )
-        {
-        int currQuat = mCornerQuat[corner];
-        int finalQuat= mQuatMult[angle][currQuat];
-        if( quatIsBad(finalQuat,corner) ) return false;
-        }
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void computePermittedAngles()
-    {
-    mPermittedDo = 0;
-
-    for(int angle=0; angle<BASIC_ANGLE; angle++)
-      {
-      if( isPermittedDo(angle ) ) mPermittedAngles[0][mPermittedDo++] = angle;
-      }
-
-    mPermittedUp = 0;
-
-    for(int angle=0; angle<BASIC_ANGLE; angle++)
-      {
-      if( isPermittedUp(angle ) ) mPermittedAngles[1][mPermittedUp++] = angle;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getNextAngle(Random rnd, int layer)
-    {
-    int num = layer==0 ? mPermittedDo:mPermittedUp;
-    int index = rnd.nextInt(num);
-    int angle = mPermittedAngles[layer][index];
-    return angle<BASIC_ANGLE/2 ? -angle : BASIC_ANGLE-angle;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getNextAngleNotZero(Random rnd, int layer)
-    {
-    int num = layer==0 ? mPermittedDo:mPermittedUp;
-    int index = rnd.nextInt(num-1);
-    int angle = mPermittedAngles[layer][index+1];
-    return angle<BASIC_ANGLE/2 ? -angle : BASIC_ANGLE-angle;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int makeQuat(int axis,int index)
-    {
-    if( axis==1 ) return 13;
-    if( index<0 ) index+=12;
-    return index;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean cornerBelongs(int index, int axis, int layer)
-    {
-    if( axis==0 )
-      {
-      boolean up = cornerIsUp(index);
-      return ((up && layer==2) || (!up && layer==0));
-      }
-    else
-      {
-      boolean le = cornerIsLeft(index);
-      return ((le && layer==0) || (!le && layer==1));
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void updateCornerQuats(int[] rotInfo)
-    {
-    if( mQuatMult==null ) initializeQuatMult();
-
-    int axis = rotInfo[0];
-    int layer= rotInfo[1];
-    int index=-rotInfo[2];
-
-    int quat = makeQuat(axis,index);
-
-    for(int corner=0; corner<8; corner++)
-      {
-      if( cornerBelongs(corner,axis,layer) )
-        {
-        int curr = mCornerQuat[corner];
-        mCornerQuat[corner] = mQuatMult[quat][curr];
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void randomizeNewScramble1(int[][] scramble, Random rnd, int curr, int total)
-    {
-    int layer, nextAngle;
-
-    if( curr==0 )
-      {
-      for(int corner=0; corner<8; corner++) mCornerQuat[corner] = 0;
-      mLastRot = rnd.nextInt(4);
-      computePermittedAngles();
-      }
-
-    switch(mLastRot)
-      {
-      case LAST_SL: layer = rnd.nextInt(2);
-                    nextAngle = getNextAngle(rnd,layer);
-
-                    if( nextAngle==0 )
-                      {
-                      layer = 1-layer;
-                      nextAngle = getNextAngleNotZero(rnd,layer);
-                      }
-
-                    scramble[curr][0] = 0;
-                    scramble[curr][1] = 2*layer;
-                    scramble[curr][2] = nextAngle;
-                    mLastRot = layer==0 ? LAST_LO : LAST_UP;
-                    updateCornerQuats(scramble[curr]);
-                    break;
-      case LAST_LO:
-      case LAST_UP: layer = mLastRot==LAST_LO ? 1:0;
-                    nextAngle = getNextAngle(rnd,layer);
-
-                    if( nextAngle!=0 )
-                      {
-                      scramble[curr][0] = 0;
-                      scramble[curr][1] = 2*layer;
-                      scramble[curr][2] = nextAngle;
-                      updateCornerQuats(scramble[curr]);
-                      mLastRot = LAST_UL;
-                      }
-                    else
-                      {
-                      scramble[curr][0] = 1;
-                      scramble[curr][1] = rnd.nextInt(2);
-                      scramble[curr][2] = 1;
-                      mLastRot = LAST_SL;
-                      updateCornerQuats(scramble[curr]);
-                      computePermittedAngles();
-                      }
-
-                    break;
-      case LAST_UL: scramble[curr][0] = 1;
-                    scramble[curr][1] = rnd.nextInt(2);
-                    scramble[curr][2] = 1;
-                    mLastRot = LAST_SL;
-                    updateCornerQuats(scramble[curr]);
-                    computePermittedAngles();
-                    break;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public void randomizeNewScramble(int[][] scramble, Random rnd, int curr, int total)
-    {
-    if( mType==0 ) randomizeNewScramble0(scramble, rnd, curr, total);
-    if( mType==1 ) randomizeNewScramble1(scramble, rnd, curr, total);
-    }
-  }
diff --git a/src/main/java/org/distorted/objects/TwistyPyraminx.java b/src/main/java/org/distorted/objects/TwistyPyraminx.java
index 3272c37f..2ed58bbb 100644
--- a/src/main/java/org/distorted/objects/TwistyPyraminx.java
+++ b/src/main/java/org/distorted/objects/TwistyPyraminx.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement4;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty4;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -60,15 +64,15 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyPyraminx(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                 DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyPyraminx(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                        DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.PYRA, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -138,7 +142,7 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -185,7 +189,7 @@ public class TwistyPyraminx extends Twisty4
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // there are (n^3-n)/6 octahedrons and ((n+1)^3 - (n+1))/6 tetrahedrons
 
-  float[][] getCubitPositions(int size)
+  protected float[][] getCubitPositions(int size)
     {
     int numOcta = (size-1)*size*(size+1)/6;
     int numTetra= size*(size+1)*(size+2)/6;
@@ -199,7 +203,7 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -207,21 +211,21 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
@@ -256,7 +260,7 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 8;
     }
@@ -272,12 +276,12 @@ public class TwistyPyraminx extends Twisty4
 
   private int faceColor(int cubit, int axis)
     {
-    return CUBITS[cubit].mRotationRow[axis] == 1 ? axis : NUM_TEXTURES;
+    return CUBITS[cubit].getRotRow(axis) == 1 ? axis : NUM_TEXTURES;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( cubit< (size-1)*size*(size+1)/6 )
       {
@@ -298,7 +302,7 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -334,7 +338,7 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     return mQuats[0];
@@ -342,21 +346,21 @@ public class TwistyPyraminx extends Twisty4
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<getNumOctahedrons(numLayers) ? 0:1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyRedi.java b/src/main/java/org/distorted/objects/TwistyRedi.java
index 349f5082..79647fad 100644
--- a/src/main/java/org/distorted/objects/TwistyRedi.java
+++ b/src/main/java/org/distorted/objects/TwistyRedi.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_CORNER;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_CORNER;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -67,15 +71,15 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyRedi(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-             DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyRedi(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                    DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.REDI, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -120,7 +124,7 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -129,7 +133,7 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -137,21 +141,21 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int size)
+  protected float[][] getCuts(int size)
     {
     if( mCuts==null )
       {
@@ -178,14 +182,14 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 9;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int size)
+  protected float[][] getCubitPositions(int size)
     {
     if( mCenters==null )
       {
@@ -223,7 +227,7 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -299,7 +303,7 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
 
@@ -333,21 +337,21 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 2;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<8 ? 0:1;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( mFaceMap==null )
       {
@@ -385,7 +389,7 @@ public class TwistyRedi extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyRex.java b/src/main/java/org/distorted/objects/TwistyRex.java
index 0ec1323e..3d71ea49 100644
--- a/src/main/java/org/distorted/objects/TwistyRex.java
+++ b/src/main/java/org/distorted/objects/TwistyRex.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_CORNER;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_CORNER;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -68,15 +72,15 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyRex(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-            DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyRex(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                   DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.REX, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -115,7 +119,7 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -124,7 +128,7 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -132,21 +136,21 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
@@ -173,14 +177,14 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     final float DIST1= 1.50f;
     final float DIST2= (1+2*REX_D)/2;
@@ -238,7 +242,7 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -285,7 +289,7 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
 
@@ -342,21 +346,21 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<24 ? 0 : (cubit<30?1:2);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( mFaceMap==null )
       {
@@ -414,7 +418,7 @@ public class TwistyRex extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistySkewb.java b/src/main/java/org/distorted/objects/TwistySkewb.java
index dc532e19..ba58dc20 100644
--- a/src/main/java/org/distorted/objects/TwistySkewb.java
+++ b/src/main/java/org/distorted/objects/TwistySkewb.java
@@ -19,19 +19,23 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_SPLIT_CORNER;
+import static org.distorted.objectlb.Movement.TYPE_SPLIT_CORNER;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -66,15 +70,15 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistySkewb(int size, Static4D quat, DistortedTexture texture,
-              MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistySkewb(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                     DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, 2*size-2, quat, texture, mesh, effects, moves, ObjectList.SKEW, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -114,7 +118,7 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -144,7 +148,7 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -152,21 +156,21 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
@@ -192,14 +196,14 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     final float DIST_CORNER = numLayers-1;
     final float DIST_EDGE   = numLayers-1;
@@ -327,7 +331,7 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int numCorners = getNumCorners();
@@ -387,7 +391,7 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -438,14 +442,14 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     int numCorners = getNumCorners();
     if( cubit<numCorners ) return 0;
@@ -455,7 +459,7 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( mCornerMap==null || mEdgeMap==null || mCenterMap==null )
       {
@@ -519,7 +523,7 @@ public class TwistySkewb extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistySquare.java b/src/main/java/org/distorted/objects/TwistySquare.java
index a1e5fbc0..57de65ae 100644
--- a/src/main/java/org/distorted/objects/TwistySquare.java
+++ b/src/main/java/org/distorted/objects/TwistySquare.java
@@ -19,7 +19,7 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
@@ -28,6 +28,10 @@ import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty6;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -110,7 +114,7 @@ abstract class TwistySquare extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -118,14 +122,14 @@ abstract class TwistySquare extends Twisty6
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 6;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistySquare1.java b/src/main/java/org/distorted/objects/TwistySquare1.java
index 045b3c92..f3db354a 100644
--- a/src/main/java/org/distorted/objects/TwistySquare1.java
+++ b/src/main/java/org/distorted/objects/TwistySquare1.java
@@ -21,18 +21,20 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.Movement6;
+import org.distorted.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistySquare1 extends TwistySquare
+public class TwistySquare1 extends TwistySquare
 {
   private static final int NUM_STICKERS = 6;
 
@@ -44,15 +46,15 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistySquare1(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistySquare1(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                       DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, quat, texture, mesh, effects, moves, ObjectList.SQU1, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     return null;
     }
@@ -66,7 +68,7 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -75,7 +77,7 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -178,7 +180,7 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     if( mQuatNumber ==null )
@@ -196,21 +198,21 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<2 ? 0 : (cubit<10 ? 1:2);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
@@ -246,7 +248,7 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     if( mCenters==null )
       {
@@ -279,21 +281,21 @@ class TwistySquare1 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return NUM_STICKERS;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( mStickerColor==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistySquare2.java b/src/main/java/org/distorted/objects/TwistySquare2.java
index 5d320b16..45c9f149 100644
--- a/src/main/java/org/distorted/objects/TwistySquare2.java
+++ b/src/main/java/org/distorted/objects/TwistySquare2.java
@@ -21,18 +21,19 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 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.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistySquare2 extends TwistySquare
+public class TwistySquare2 extends TwistySquare
 {
   private static final int NUM_STICKERS = 6;
 
@@ -45,15 +46,15 @@ class TwistySquare2 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistySquare2(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
-                DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistySquare2(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                       DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, quat, texture, mesh, effects, moves, ObjectList.SQU2, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -78,14 +79,14 @@ class TwistySquare2 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     return null;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -182,7 +183,7 @@ class TwistySquare2 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
 
@@ -202,21 +203,21 @@ class TwistySquare2 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<2 ? 0 : (cubit<10 ? 1:2);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
@@ -252,7 +253,7 @@ class TwistySquare2 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     if( mCenters ==null )
       {
@@ -297,21 +298,21 @@ class TwistySquare2 extends TwistySquare
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return NUM_STICKERS;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int numLayers)
+  protected int getFaceColor(int cubit, int cubitface, int numLayers)
     {
     if( mStickerColor==null )
       {
diff --git a/src/main/java/org/distorted/objects/TwistyUltimate.java b/src/main/java/org/distorted/objects/TwistyUltimate.java
index 70ab3a73..a7e3dfa8 100644
--- a/src/main/java/org/distorted/objects/TwistyUltimate.java
+++ b/src/main/java/org/distorted/objects/TwistyUltimate.java
@@ -19,23 +19,27 @@
 
 package org.distorted.objects;
 
-import static org.distorted.objects.Movement.TYPE_NOT_SPLIT;
+import static org.distorted.objectlb.Movement.TYPE_NOT_SPLIT;
 
 import android.content.res.Resources;
 
-import org.distorted.helpers.ObjectShape;
-import org.distorted.helpers.ObjectSticker;
-import org.distorted.helpers.ScrambleState;
+import org.distorted.objectlb.ObjectShape;
+import org.distorted.objectlb.ObjectSticker;
+import org.distorted.objectlb.ScrambleState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 import org.distorted.main.R;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.Movement12;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.Twisty12;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class TwistyUltimate extends Twisty12
+public class TwistyUltimate extends Twisty12
 {
   private static final float A = (float)Math.sqrt(21*SQ5+47);
   private static final float B = SQ6*(5*SQ5+11)/(6*A);
@@ -71,15 +75,15 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  TwistyUltimate(int size, Static4D quat, DistortedTexture texture,
-                 MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+  public TwistyUltimate(int size, Static4D quat, DistortedTexture texture, MeshSquare mesh,
+                        DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
     {
     super(size, size, quat, texture, mesh, effects, moves, ObjectList.ULTI, res, scrWidth);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ScrambleState[] getScrambleStates()
+  protected ScrambleState[] getScrambleStates()
     {
     if( mStates==null )
       {
@@ -117,7 +121,7 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[] getSolvedQuats(int cubit, int numLayers)
+  protected int[] getSolvedQuats(int cubit, int numLayers)
     {
     if( mQuats==null ) initializeQuats();
     int status = retCubitSolvedStatus(cubit,numLayers);
@@ -126,7 +130,7 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectShape getObjectShape(int cubit, int numLayers)
+  protected ObjectShape getObjectShape(int cubit, int numLayers)
     {
     int variant = getCubitVariant(cubit,numLayers);
 
@@ -232,7 +236,7 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D getQuat(int cubit, int numLayers)
+  protected Static4D getQuat(int cubit, int numLayers)
     {
     if( mQuats     ==null ) initializeQuats();
     if( mQuatIndex ==null ) mQuatIndex = new int[] { 0,6,1,2,0,4,6,5,0,1,4,9,5,2 };
@@ -241,21 +245,21 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitVariants(int numLayers)
+  protected int getNumCubitVariants(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getCubitVariant(int cubit, int numLayers)
+  protected int getCubitVariant(int cubit, int numLayers)
     {
     return cubit<4 ? 0 : (cubit<8 ? 1:2);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCubitPositions(int numLayers)
+  protected float[][] getCubitPositions(int numLayers)
     {
     if( mCenters==null )
       {
@@ -285,7 +289,7 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getFaceColor(int cubit, int cubitface, int size)
+  protected int getFaceColor(int cubit, int cubitface, int size)
     {
     if( mFaceMap==null )
       {
@@ -315,7 +319,7 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  ObjectSticker retSticker(int face)
+  protected ObjectSticker retSticker(int face)
     {
     if( mStickers==null )
       {
@@ -345,7 +349,7 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  Static4D[] getQuats()
+  protected Static4D[] getQuats()
     {
     if( mQuats==null ) initializeQuats();
     return mQuats;
@@ -353,14 +357,14 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getSolvedFunctionIndex()
+  protected int getSolvedFunctionIndex()
     {
     return 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  float[][] getCuts(int numLayers)
+  protected float[][] getCuts(int numLayers)
     {
     if( mCuts==null )
       {
@@ -387,14 +391,14 @@ class TwistyUltimate extends Twisty12
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumStickerTypes(int numLayers)
+  protected int getNumStickerTypes(int numLayers)
     {
     return 3;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int getNumCubitFaces()
+  protected int getNumCubitFaces()
     {
     return 8;
     }
diff --git a/src/main/java/org/distorted/patterns/RubikPatternList.java b/src/main/java/org/distorted/patterns/RubikPatternList.java
index 5764ec8e..43593f7b 100644
--- a/src/main/java/org/distorted/patterns/RubikPatternList.java
+++ b/src/main/java/org/distorted/patterns/RubikPatternList.java
@@ -19,7 +19,7 @@
 
 package org.distorted.patterns;
 
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenAbstract.java b/src/main/java/org/distorted/screens/RubikScreenAbstract.java
index 81b20bfe..46e69c16 100644
--- a/src/main/java/org/distorted/screens/RubikScreenAbstract.java
+++ b/src/main/java/org/distorted/screens/RubikScreenAbstract.java
@@ -22,7 +22,7 @@ package org.distorted.screens;
 import android.content.SharedPreferences;
 
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.patterns.RubikPatternList;
 import org.distorted.tutorials.TutorialList;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenPattern.java b/src/main/java/org/distorted/screens/RubikScreenPattern.java
index 024067c2..98369033 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPattern.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPattern.java
@@ -35,7 +35,7 @@ import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.patterns.RubikPattern;
 import org.distorted.patterns.RubikPatternList;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenPlay.java b/src/main/java/org/distorted/screens/RubikScreenPlay.java
index 32a95cad..0e58219d 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPlay.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPlay.java
@@ -44,7 +44,7 @@ import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.network.RubikScores;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolution.java b/src/main/java/org/distorted/screens/RubikScreenSolution.java
index f4a704d6..fa6fa86e 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolution.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolution.java
@@ -33,7 +33,7 @@ import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.TwistyObject;
 import org.distorted.patterns.RubikPattern;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolver.java b/src/main/java/org/distorted/screens/RubikScreenSolver.java
index 06883e58..04716251 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolver.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolver.java
@@ -36,8 +36,8 @@ import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
-import org.distorted.objects.TwistyObject;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.TwistyObject;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.solvers.ImplementedSolversList;
 import org.distorted.solvers.SolverMain;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolving.java b/src/main/java/org/distorted/screens/RubikScreenSolving.java
index 1caed9f6..9620819c 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolving.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolving.java
@@ -31,7 +31,7 @@ import org.distorted.dialogs.RubikDialogAbandon;
 import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.network.RubikScores;
 
 import java.util.Timer;
diff --git a/src/main/java/org/distorted/solvers/ImplementedSolversList.java b/src/main/java/org/distorted/solvers/ImplementedSolversList.java
index 1473ffb5..bc28740f 100644
--- a/src/main/java/org/distorted/solvers/ImplementedSolversList.java
+++ b/src/main/java/org/distorted/solvers/ImplementedSolversList.java
@@ -19,7 +19,7 @@
 
 package org.distorted.solvers;
 
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/solvers/SolverMain.java b/src/main/java/org/distorted/solvers/SolverMain.java
index 54e744cf..d28ce1d7 100644
--- a/src/main/java/org/distorted/solvers/SolverMain.java
+++ b/src/main/java/org/distorted/solvers/SolverMain.java
@@ -22,8 +22,8 @@ package org.distorted.solvers;
 import android.content.res.Resources;
 
 import org.distorted.main.R;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.TwistyObject;
 import org.distorted.screens.ScreenList;
 import org.distorted.screens.RubikScreenSolver;
 
diff --git a/src/main/java/org/distorted/tutorials/TutorialActivity.java b/src/main/java/org/distorted/tutorials/TutorialActivity.java
index a17a0a21..407bc4ff 100644
--- a/src/main/java/org/distorted/tutorials/TutorialActivity.java
+++ b/src/main/java/org/distorted/tutorials/TutorialActivity.java
@@ -36,8 +36,8 @@ import org.distorted.helpers.TwistyActivity;
 import org.distorted.helpers.TwistyPreRender;
 import org.distorted.library.main.DistortedLibrary;
 import org.distorted.main.R;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.TwistyObject;
 
 import static org.distorted.main.RubikRenderer.BRIGHTNESS;
 
diff --git a/src/main/java/org/distorted/tutorials/TutorialList.java b/src/main/java/org/distorted/tutorials/TutorialList.java
index dcd6d173..5fc9d5ae 100644
--- a/src/main/java/org/distorted/tutorials/TutorialList.java
+++ b/src/main/java/org/distorted/tutorials/TutorialList.java
@@ -19,7 +19,7 @@
 
 package org.distorted.tutorials;
 
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/tutorials/TutorialPreRender.java b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
index e3c1142c..42a7e656 100644
--- a/src/main/java/org/distorted/tutorials/TutorialPreRender.java
+++ b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
@@ -27,8 +27,8 @@ import org.distorted.effects.EffectController;
 import org.distorted.helpers.BlockController;
 import org.distorted.helpers.MovesFinished;
 import org.distorted.helpers.TwistyPreRender;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.ObjectList;
+import org.distorted.objectlb.TwistyObject;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/tutorials/TutorialState.java b/src/main/java/org/distorted/tutorials/TutorialState.java
index 1408dfa9..ad721670 100644
--- a/src/main/java/org/distorted/tutorials/TutorialState.java
+++ b/src/main/java/org/distorted/tutorials/TutorialState.java
@@ -28,7 +28,7 @@ import org.distorted.helpers.TwistyActivity;
 import org.distorted.helpers.TwistyPreRender;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.objects.ObjectList;
+import org.distorted.objectlb.ObjectList;
 import org.distorted.screens.RubikScreenPlay;
 import org.distorted.screens.ScreenList;
 import org.distorted.helpers.TransparentImageButton;
diff --git a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
index 0d052bc8..866f9d60 100644
--- a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
+++ b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
@@ -30,11 +30,11 @@ import android.view.MotionEvent;
 
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
-import org.distorted.helpers.QuatHelper;
+import org.distorted.objectlb.QuatHelper;
 import org.distorted.library.type.Static2D;
 import org.distorted.library.type.Static4D;
-import org.distorted.objects.Movement;
-import org.distorted.objects.TwistyObject;
+import org.distorted.objectlb.Movement;
+import org.distorted.objectlb.TwistyObject;
 
 import static org.distorted.main.RubikSurfaceView.INVALID_POINTER_ID;
 import static org.distorted.main.RubikSurfaceView.NUM_SPEED_PROBES;
