commit 5718b29d382b5869f371ff59cecbacac3df6b67e
Author: leszek <leszek@koltunski.pl>
Date:   Fri Dec 13 11:24:20 2024 +0100

    Carve out another, 2nd layer from TwistyObject: everything related to creating its Mesh.

diff --git a/src/main/java/org/distorted/objectlib/helpers/ObjectShape.java b/src/main/java/org/distorted/objectlib/helpers/ObjectShape.java
index f6eee2bf..11246376 100644
--- a/src/main/java/org/distorted/objectlib/helpers/ObjectShape.java
+++ b/src/main/java/org/distorted/objectlib/helpers/ObjectShape.java
@@ -12,7 +12,7 @@ package org.distorted.objectlib.helpers;
 import org.distorted.library.helpers.QuatHelper;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
-import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objectlib.main.TwistyObjectWithMesh;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -121,56 +121,11 @@ public class ObjectShape
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public int getNumFaces()
-    {
-    return mNumFaces;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean isMultigon()
-    {
-    return mFacesMultigon;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[][] getVertices()
-    {
-    return mVertices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[][] getVertIndices()
-    {
-    return mVertIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[][][] getMultigonIndices()
-    {
-    return mMultigonIndices;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getFirstVertexInFace(int face)
-    {
-    if( mFacesMultigon )
-      {
-      int[][][] indices=getMultigonIndices();
-      int vertIndex=indices[face][0][0];
-      return getVertices()[vertIndex];
-      }
-    else
-      {
-      int[][] indices=getVertIndices();
-      int vertIndex=indices[face][0];
-      return getVertices()[vertIndex];
-      }
-    }
+  public int getNumFaces()               { return mNumFaces; }
+  public boolean isMultigon()            { return mFacesMultigon; }
+  public float[][] getVertices()         { return mVertices; }
+  public int[][] getVertIndices()        { return mVertIndices; }
+  public int[][][] getMultigonIndices()  { return mMultigonIndices; }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -375,7 +330,7 @@ public class ObjectShape
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public static int[][] computeColors(ObjectShape[] shapes, float[][] moves, Static4D[] quats, TwistyObject object)
+  public static int[][] computeColors(ObjectShape[] shapes, float[][] moves, Static4D[] quats, TwistyObjectWithMesh object)
     {
     int numCubits = moves.length;
     int[][] colors = new int[numCubits][];
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObject.java b/src/main/java/org/distorted/objectlib/main/TwistyObject.java
index b1a0dd7c..09a6e924 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyObject.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObject.java
@@ -1,5 +1,5 @@
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
+// Copyright 2024 Leszek Koltunski                                                               //
 //                                                                                               //
 // This file is part of Magic Cube.                                                              //
 //                                                                                               //
@@ -9,8 +9,6 @@
 
 package org.distorted.objectlib.main;
 
-import java.io.DataInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.util.Random;
 
@@ -22,29 +20,18 @@ 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.VertexEffectSink;
-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.helpers.QuatHelper;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.mesh.MeshFile;
-import org.distorted.library.mesh.MeshJoined;
 import org.distorted.library.message.EffectListener;
-import org.distorted.library.type.Static1D;
 import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 
 import org.distorted.objectlib.helpers.FactoryCubit;
 import org.distorted.objectlib.helpers.FactorySticker;
-import org.distorted.objectlib.helpers.ObjectFaceShape;
 import org.distorted.objectlib.helpers.ObjectLibInterface;
-import org.distorted.objectlib.helpers.ObjectShape;
 import org.distorted.objectlib.helpers.ObjectSticker;
 import org.distorted.objectlib.helpers.ObjectStickerOverride;
-import org.distorted.objectlib.helpers.ObjectVertexEffects;
 import org.distorted.objectlib.helpers.OperatingSystemInterface;
 import org.distorted.objectlib.json.JsonReader;
 import org.distorted.objectlib.metadata.Metadata;
@@ -59,7 +46,7 @@ import static org.distorted.objectlib.touchcontrol.TouchControl.*;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public abstract class TwistyObject extends TwistyObjectTheoretical
+public abstract class TwistyObject extends TwistyObjectWithMesh
   {
   public static final int MODE_ICON = 0;
   public static final int MODE_NORM = 1;
@@ -70,53 +57,34 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
   private static final int DEFAULT_TEXTURE_HEIGHT = 256;
   private static final int DEFAULT_TEXTURE_ROWS   = 8;
 
-  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 MAX_SIZE_CHANGE = 1.35f;
   private static final float MIN_SIZE_CHANGE = 0.75f;
 
-  private static final Static3D CENTER = new Static3D(0,0,0);
-
   protected float[][][][] mStickerCoords;
 
   private int[][] mStickerVariants;
   private float[] mStickerScales;
-  private Static4D[] mOrigQuat;
   private TwistyObjectSolved mSolved;
-  private MeshBase[] mMeshes;
   private int mNumFaceColors, mNumTextures, mNumOverrides;
-  private int mNumCubitFaces, mNumStickerTypes;
-  private Static4D[] mMixupModeQuats;
-  private boolean mIsInMixupMode;
+  private int mNumStickerTypes;
   private Static4D mQuat;
-  private int[] mNumLayers;
   private float mSize;
-  private DistortedEffects mEffects;
   private Static3D mObjectScale;
   private DistortedTexture mTexture;
   private float mInitScreenRatio;
   private float mObjectScreenRatio;
   private int mNumTexRows, mNumTexCols, mTexHeight;
-  private MeshBase mMesh;
   private ObjectScrambler mScrambler;
   private TouchControl mTouchControl;
   private DistortedNode mNode;
   private ObjectLibInterface mInterface;
   private Bitmap mBitmap;
   private ObjectSticker[] mStickers;
-  private ObjectShape[] mShapes;
-  private int mNumCubitVariants;
-  private int[][] mCubitFaceColors;
-  private int[][] mVariantFaceIsOuter;
   private int mIconMode;
   private Metadata mMetadata;
   private ObjectStickerOverride[] mStickerOverrides;
   private boolean mError;
   private String mErrorString;
-  private int mMaxNumLayers;
   private TwistyLayerRotations mRotation;
   private int[] mColorTable;
   private int[] mOriginalColorTable;
@@ -166,36 +134,13 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
 
   private void initialize(int iconMode, Static4D quat, Static3D move, float scale, InitAssets asset, boolean fromJSON)
     {
-    Static3D[] axis = getRotationAxis();
-    int[][] angles = getBasicAngles();
-    Static3D[] faceAxis = getFaceAxis();
-    float[][] positions = getCubitPositions(mNumLayers);
-    float[][] cuts = getCuts(mNumLayers);
-    int[][] minCubits = getMinimalCubiesInRow();
-
-    int numCubits = positions.length;
-    float[][] offsets = new float[numCubits][];
-    int[] types = new int[numCubits];
-
-    for(int c=0; c<numCubits; c++)
-      {
-      offsets[c] = getCubitRowOffset(c);
-      types[c] = getCubitRotationType(c);
-      }
-
-    super.initialize(axis, angles, positions, cuts, minCubits, offsets, types, faceAxis);
+    super.initialize(asset,fromJSON);
 
     mIconMode = iconMode;
     mQuat = quat;
     mInitScreenRatio = getScreenRatio();
     mCubitQuats = new int[mNumCubits];
 
-    mMaxNumLayers = -1;
-    for(int i=0; i<mNumAxis; i++)
-      {
-      if( mMaxNumLayers<mNumLayers[i] ) mMaxNumLayers = mNumLayers[i];
-      }
-
     mOriginalColorTable = getColorTable();
     mNumFaceColors = mOriginalColorTable.length;
     mColorTable = new int[mNumFaceColors];
@@ -214,9 +159,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     mObjectScale = new Static3D(scale,scale,scale);
     setObjectRatioNow(scale,720);
 
-    mEffects = new DistortedEffects();
-    createQuaternionEffects();
-
     mRotation = new TwistyLayerRotations(this,mAxis,mNumLayers,getGhostAngle(),mEffects);
 
     MatrixEffectScale scaleEffect = new MatrixEffectScale(mObjectScale);
@@ -225,8 +167,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
 
     InputStream meshStream = asset!=null ? asset.getMeshStream(): null;
     boolean fromDMESH = (meshStream!=null);
-    getQuatsAndShapes(fromDMESH,fromJSON);
-    createMesh(meshStream,fromDMESH);
     setUpTextures(fromDMESH,fromJSON);
 
     int index = getSolvedFunctionIndex();
@@ -239,8 +179,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     mEffects.apply(moveEffect);
 
     mNode = new DistortedNode(mTexture,mEffects,mMesh);
-
-    for(int c=0; c<mNumCubits; c++) setCubitQuat(c,0);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -267,186 +205,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return ImplementedTablebasesList.createPacked(os,shortName);
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createQuaternionEffects()
-    {
-    if( mNumQuats<=ObjectControl.MAX_QUATS )
-      {
-      mIsInMixupMode = false;
-
-      for( int q=0; q<mNumQuats; q++)
-        {
-        VertexEffectQuaternion vq = new VertexEffectQuaternion(mObjectQuats[q],CENTER);
-        vq.setMeshAssociation(0,q);
-        mEffects.apply(vq);
-        }
-      }
-    else if( mNumCubits<=ObjectControl.MAX_QUATS )
-      {
-      mIsInMixupMode = true;
-      mMixupModeQuats = new Static4D[mNumCubits];
-
-      for( int q=0; q<mNumCubits; q++)
-        {
-        mMixupModeQuats[q] = new Static4D(mObjectQuats[0]);
-        VertexEffectQuaternion vq = new VertexEffectQuaternion(mMixupModeQuats[q],CENTER);
-        vq.setMeshAssociation(0,q);
-        mEffects.apply(vq);
-        }
-      }
-    else
-      {
-      android.util.Log.e("D", "object has too many quaternions ("+mNumQuats+") or too many cubits ("+mNumCubits+")");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 createOuterFaces()
-    {
-    for(int v=0; v<mNumCubitVariants; v++)
-      {
-      int numFaces = mShapes[v].getNumFaces();
-      mVariantFaceIsOuter[v] = new int[numFaces];
-      }
-
-    for( int cubit=0; cubit<mNumCubits; cubit++)
-      {
-      int variant = getCubitVariant(cubit,mNumLayers);
-      int numFaces = mShapes[variant].getNumFaces();
-
-      for(int face=0; face<numFaces; face++)
-        if( getDefaultCubitFaceColor(cubit,face)>=0 )
-          {
-          mVariantFaceIsOuter[variant][face] = 1;
-          }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void getQuatsAndShapes(boolean fromDMESH, boolean fromJSON)
-    {
-    mNumCubitVariants = getNumCubitVariants(mNumLayers);
-
-    if( !fromDMESH || !fromJSON )
-      {
-      FactoryCubit factory = FactoryCubit.getInstance();
-      factory.clear();
-
-      mShapes = new ObjectShape[mNumCubitVariants];
-      for(int v=0; v<mNumCubitVariants; v++) mShapes[v] = getObjectShape(v);
-      mNumCubitFaces = ObjectShape.computeNumComponents(mShapes);
-      mVariantFaceIsOuter = new int[mNumCubitVariants][];
-
-      mOrigQuat = new Static4D[mNumCubits];
-      for(int i=0; i<mNumCubits; i++) mOrigQuat[i] = getCubitQuats(i,mNumLayers);
-
-      if( !fromJSON )
-        {
-        mCubitFaceColors = ObjectShape.computeColors(mShapes,mCubitPos,mOrigQuat,this);
-        createOuterFaces();
-        }
-
-      if( fromDMESH )
-        {
-        for(int i=0; i<mNumCubitVariants; i++) factory.createNewFaceTransform(mShapes[i], mVariantFaceIsOuter[i]);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createMesh(InputStream stream, boolean fromDMESH)
-    {
-    if( fromDMESH )
-      {
-      DataInputStream dos = new DataInputStream(stream);
-      mMesh = new MeshFile(dos);
-
-      try
-        {
-        stream.close();
-        }
-      catch(IOException e)
-        {
-        android.util.Log.e("meshFile", "Error closing InputStream: "+e);
-        }
-      }
-    else
-      {
-      MeshBase[] cubitMesh = new MeshBase[mNumCubits];
-
-      for(int i=0; i<mNumCubits; i++)
-        {
-        cubitMesh[i] = createCubitMesh(i,mNumLayers,mNumCubitFaces);
-        Static3D pos = getPos(mCubitPos[i]);
-        cubitMesh[i].apply(new MatrixEffectMove(pos),1,0);
-        }
-
-      mMesh = new MeshJoined(cubitMesh);
-
-      float pillowCoeff = getPillowCoeff();
-
-      if( pillowCoeff!=1.0f )
-        {
-        float radius = getCircumscribedRadius();
-        Static1D coeff = new Static1D(pillowCoeff);
-        Static4D region= new Static4D(0,0,0,radius);
-        VertexEffectSink sink = new VertexEffectSink(coeff,CENTER,region);
-        mMesh.apply(sink);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private MeshBase createCubitMesh(int cubit, int[] numLayers, int numComponents)
-    {
-    int variant = getCubitVariant(cubit,numLayers);
-
-    if( mMeshes==null ) mMeshes = new MeshBase[mNumCubitVariants];
-
-    if( mMeshes[variant]==null )
-      {
-      ObjectFaceShape faceShape = getObjectFaceShape(variant);
-      ObjectVertexEffects effects = getVertexEffects(variant);
-      FactoryCubit factory = FactoryCubit.getInstance();
-      factory.createNewFaceTransform(mShapes[variant],mVariantFaceIsOuter[variant]);
-      mMeshes[variant] = factory.createRoundedSolid(mShapes[variant],faceShape,effects,numComponents);
-      }
-
-    MeshBase mesh = mMeshes[variant].copy(true);
-    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( mOrigQuat[cubit], CENTER );
-    mesh.apply(quat,0xffffffff,0);
-
-    return mesh;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void figureOutBitmapDimensions(int numTextures)
@@ -476,7 +234,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     if( fromJSON )
       {
       mNumStickerTypes = getNumStickerTypes();
-      mNumCubitFaces = getNumCubitFaces();
       }
     else
       {
@@ -541,140 +298,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return ScrambleEdgeGenerator.getScramblingAlgorithms(mBasicAngles);
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void rotateAllCubits(int axisIndex, int rowBitmap, int angle)
-    {
-    super.rotateAllCubits(axisIndex,rowBitmap,angle);
-    for(int i=0; i<mNumCubits; i++) setCubitQuat(i,getCubitQuatIndex(i));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean sticksOut(Static3D[] faceAxis, float[] dist, float x, float y, float z )
-    {
-    final float MAXERR = 0.05f;
-    int numAxis = dist.length;
-
-    for(int i=0; i<numAxis; i++)
-      {
-      Static3D ax = faceAxis[i];
-      float len = ax.get0()*x + ax.get1()*y + ax.get2()*z;
-      if( len>mSize*dist[i]+MAXERR ) return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean doesNotStickOut(int variant, float px, float py, float pz, float[] tmp, Static4D quat)
-    {
-    float[][] vertices = mShapes[variant].getVertices();
-    Static3D[] axis = getFaceAxis();
-    float[] dist3D = getDist3D(mNumLayers);
-
-    for( float[] vertex : vertices)
-      {
-      float x = vertex[0];
-      float y = vertex[1];
-      float z = vertex[2];
-
-      QuatHelper.rotateVectorByQuat(tmp, x, y, z, 1, quat);
-
-      float mx = tmp[0] + px;
-      float my = tmp[1] + py;
-      float mz = tmp[2] + pz;
-
-      if( sticksOut(axis, dist3D, mx,my,mz) ) return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float computeAvg(float[] pos, int offset)
-    {
-    int len = pos.length/3;
-    float ret=0.0f;
-    for(int i=0; i<len; i++) ret += pos[3*i+offset];
-    ret /= len;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  protected void displayCubitQuats()
-    {
-    StringBuilder builder = new StringBuilder();
-    float[] tmp = new float[4];
-    float ERR = 0.01f;
-
-    for(int cubit=0; cubit<mNumCubits; cubit++)
-      {
-      builder.append(cubit);
-      builder.append(" : ");
-
-      int refCubit,variant = getCubitVariant(cubit,mNumLayers);
-
-      for(refCubit=0; refCubit<mNumCubits; refCubit++)
-        {
-        if( getCubitVariant(refCubit,mNumLayers)==variant ) break;
-        }
-
-      float[] curpos = mCubitPos[cubit];
-      float[] refpos = mCubitPos[refCubit];
-      float refX = computeAvg(refpos,0);
-      float refY = computeAvg(refpos,1);
-      float refZ = computeAvg(refpos,2);
-      float curX = computeAvg(curpos,0);
-      float curY = computeAvg(curpos,1);
-      float curZ = computeAvg(curpos,2);
-
-      for(int quat=0; quat<mNumQuats; quat++)
-        {
-        QuatHelper.rotateVectorByQuat(tmp,refX,refY,refZ,0,mObjectQuats[quat]);
-
-        float dx = tmp[0]-curX;
-        float dy = tmp[1]-curY;
-        float dz = tmp[2]-curZ;
-
-        if( dx>-ERR && dx<ERR && dy>-ERR && dy<ERR && dz>-ERR && dz<ERR )
-          {
-          if( doesNotStickOut(variant,curX,curY,curZ,tmp,mObjectQuats[quat]) )
-            {
-            builder.append(quat);
-            builder.append(',');
-            }
-          else
-            {
-            android.util.Log.e("D", "cubit: "+cubit+" quat: "+quat+" : center correct, but shape sticks out");
-            }
-          }
-        }
-
-      builder.append('\n');
-      }
-
-    android.util.Log.e("D", "cubitQuats: \n"+builder );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCubitRotationType(int cubit)
-    {
-    return TwistyObjectTheoreticalCubit.TYPE_NORMAL;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public float[] getCubitRowOffset(int cubitIndex)
-    {
-    return null;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public int[][] getSolvedQuats()
@@ -686,7 +309,7 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
 
   protected int[][] getOldSolvedQuats()
     {
-    return mSolved.getSolvedQuats(mNumLayers,mNumCubitFaces,mCubitFaceColors);
+    return mSolved.getSolvedQuats(mNumLayers,mNumCubitFaces);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -709,13 +332,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return ObjectScrambler.SCRAMBLING_ALGORITHMS;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[][] getMinimalCubiesInRow()
-    {
-    return null;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   void setLibInterface(ObjectLibInterface inter)
@@ -829,50 +445,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return puzzleFace;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCubitFaceMap(int cubit, int face)
-    {
-    int numFaces = mCubitFaceColors[cubit].length;
-    int puzzleFace = face<numFaces ? mCubitFaceColors[cubit][face] : -1;
-    return puzzleFace<0 ? -1 : puzzleFace;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// this would return the REAL value of outer; the 'getCubitFaceColor()>=0' returns if the face is
-// colored, which for example in case of Container (which has colored internal faces) is not the
-// same thing
-
-  boolean faceIsOuter(int cubit, int face)
-    {
-    if( mCubitFaceColors==null )
-      {
-      if( mShapes==null )
-        {
-        mShapes = new ObjectShape[mNumCubitVariants];
-        }
-
-      if( mShapes[0]==null )
-        {
-        for(int v=0; v<mNumCubitVariants; v++) mShapes[v] = getObjectShape(v);
-        }
-
-      if( mOrigQuat==null )
-        {
-        mOrigQuat = new Static4D[mNumCubits];
-        }
-
-      if( mOrigQuat[0]==null )
-        {
-        for(int c=0; c<mNumCubits; c++) mOrigQuat[c] = getCubitQuats(c,mNumLayers);
-        }
-
-      mCubitFaceColors = ObjectShape.computeColors(mShapes,mCubitPos,mOrigQuat,this);
-      }
-
-    return mCubitFaceColors[cubit][face]>=0;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   void resetAllTextureMaps()
@@ -936,8 +508,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     super.releaseResources();
 
     mTexture.markForDeletion();
-    mMesh.markForDeletion();
-    mEffects.markForDeletion();
 
     if( mBitmap!=null )
       {
@@ -946,40 +516,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
       }
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int computeAssociation(int cubit)
-    {
-    int result = 0, accumulativeShift = 0;
-
-    for(int axis=0; axis<mNumAxis; axis++)
-      {
-      int rotRow = getCubitRotRow(cubit,axis);
-      result += (rotRow<<accumulativeShift);
-      accumulativeShift += mMaxNumLayers;
-      }
-
-    return result;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setCubitQuat(int cubit, int equAssociation)
-    {
-    int andAssociation = computeAssociation(cubit);
-
-    if( !mIsInMixupMode )
-      {
-      mMesh.setEffectAssociation(cubit,andAssociation,equAssociation);
-      }
-    else
-      {
-      mMesh.setEffectAssociation(cubit,andAssociation,cubit);
-      Static4D tmp = mObjectQuats[equAssociation];
-      mMixupModeQuats[cubit].set(tmp);
-      }
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private int numberOfDifferentColors(int[] table)
@@ -1032,21 +568,7 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
   synchronized void restorePreferences(OperatingSystemInterface os)
     {
     String key = getShortName();
-    boolean error = super.restorePreferences(key,os);
-
-    if( error )
-      {
-      for(int c=0; c<mNumCubits; c++) setCubitQuat(c,0);
-      }
-    else
-      {
-      for(int c=0; c<mNumCubits; c++)
-        {
-        int q = getCubitQuatIndex(c);
-        setCubitQuat(c, q);
-        }
-      }
-
+    super.restorePreferences(key,os);
     restoreSti(key,os);
     }
 
@@ -1145,30 +667,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return 1-A*cos;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Radius of the sphere circumscribed on the puzzle. Needed for pillowing.
-//
-// This won't work correctly for pillowing off-center puzzles (e.g. mirrors) - for those we'd need
-// to introduce the concept of a 'sink center' as well.
-//
-// public because needed in TouchControlShapemod
-
-  public float getCircumscribedRadius()
-    {
-    switch( getCategory() )
-      {
-      case Metadata.CATEGORY_SHAPE_TET: return (SQ6/4)*mSize;
-      case Metadata.CATEGORY_SHAPE_HEX: return (SQ3/2)*mSize;
-      case Metadata.CATEGORY_SHAPE_OCT: return (SQ2/2)*mSize;
-      case Metadata.CATEGORY_SHAPE_DOD: return (SQ3/2)*((SQ5+1)/2)*mSize;
-      case Metadata.CATEGORY_SHAPE_BAL: return 0.50f*mSize;
-      case Metadata.CATEGORY_SHAPE_ICO: return 0; // no pillowed icosahedra
-      case Metadata.CATEGORY_SHAPE_BAR: return 0; // no pillowed barrels
-      }
-
-    return 0.0f;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // might be overridden in subclasses which want per-edge radii
 
@@ -1268,60 +766,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
 
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Return the 'surface' the given face of the given cubit is on. (px,py,pz) is the pre-computed
-// center of the cubit (so we don't have to keep re-computing this per each face)
-// If the face is not external, return null.
-//
-// This is needed by the TwistyObjectSolved's method2 detecting if the object is in a solved state.
-//
-// What is a 'surface' ? For objects with flat monochromatic external walls, it is simply a 4-tuple
-// [(nx,ny,nz),d] (normal vector + distance from origin) describing a 3D plane this face is part of.
-// Exceptions are objects with curved external walls (ATM Masterball & Penrose Cubes): it is a union
-// of several surfaces which together define the bent, monochromatic wall. I.e. Penrose Cube has 3
-// surfaces (blue, yellow and red), each defined by 3 planes (think before the bending effect!)
-//
-// This is the default which simply returns the single plane. If some puzzle has more complicated
-// surfaces, it needs to override this method.
-//
-// We never need to test if a given point is part of a given surface (see TwistyObjectSurface!) - we
-// only need to be able to rotate the surface by quats and test if two surfaces are the same. Thus:
-// it is possible to provide some artificial surfaces - for example, in case of the Penrose, the
-// three surfaces can be only the single 'middle' plane [the one on the bend].
-
-  public float[] getMonochromaticSurface(int variant, int cubitIndex, int faceIndex, float px, float py, float pz)
-    {
-    if( !faceIsOuter(cubitIndex,faceIndex) ) return null;
-
-    Static4D cubitQuat = getCubitQuats(cubitIndex,mNumLayers);
-
-    float[] normal   = new float[4];
-    float[] intPoint = new float[3];
-    float[] rotPoint = new float[4];
-    float[] surface  = new float[4];
-
-    ObjectShape objShape = mShapes[variant];
-    objShape.getFacePoint(faceIndex,intPoint);
-    objShape.getFaceNormal(faceIndex,normal);
-
-    QuatHelper.rotateVectorByQuat(rotPoint,intPoint[0],intPoint[1],intPoint[2],1,cubitQuat);
-    QuatHelper.rotateVectorByQuat(surface,normal[0],normal[1],normal[2],0,cubitQuat);
-
-    float x = rotPoint[0] + px;
-    float y = rotPoint[1] + py;
-    float z = rotPoint[2] + pz;
-    surface[3] = x*surface[0] + y*surface[1] + z*surface[2];
-
-    return surface;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[][] getVariantFaceIsOuter()
-    {
-    return mVariantFaceIsOuter;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public int getInternalColor()
@@ -1506,20 +950,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     mEffects.abortById(effectID);
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public MeshBase getObjectMesh()
-    {
-    return mMesh;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public DistortedEffects getObjectEffects()
-    {
-    return mEffects;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public ObjectStickerOverride[] getStickerOverrides()
@@ -1555,21 +985,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return (mNumTexRows-1-y)*mNumTexCols + x;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getNumLayers()
-    {
-    return mNumLayers;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void solve()
-    {
-    super.solve();
-    for(int i=0; i<mNumCubits; i++) setCubitQuat(i,0);
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public Bitmap getStickerBitmap()
@@ -1606,24 +1021,6 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
     return builder.toString();
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// this is here only so it can be overridden in TwistyJSON so that we can get this from JSON.
-
-  public int getNumCubitFaces()
-    {
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 1.0 - i.e. no pillowing - by default.
-// The coeff is really param of the 'sink' vertex effect - if it is not equal to 1.0, we apply the
-// sink effect [centered at (0,0,0)] to the whole mesh as the last step of composing it.
-
-  public float getPillowCoeff()
-    {
-    return 1.0f;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public TouchControl getTouchControl()
@@ -1687,33 +1084,19 @@ public abstract class TwistyObject extends TwistyObjectTheoretical
   public boolean isSubmittable()              { return mCurrentColorSchemeSubmittable; }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-  // for JSON only
+
   public abstract int getTouchControlType();
   public abstract int getTouchControlSplit();
   public abstract boolean[][] getLayerRotatable(int[] numLayers);
   public abstract int[][][] getEnabled();
-  public abstract float[] getDist3D(int[] numLayers);
-  public abstract Static3D[] getFaceAxis();
   public abstract int[][] getScrambleEdges();
-  public abstract float[][] getCuts(int[] numLayers);
   public abstract float getStickerRadius();
   public abstract float getStickerStroke();
   public abstract float[][][] getStickerAngles();
-  public abstract int getCubitVariant(int cubit, int[] numLayers);
-  public abstract ObjectShape getObjectShape(int variant);
-  public abstract ObjectFaceShape getObjectFaceShape(int variant);
-  public abstract ObjectVertexEffects getVertexEffects(int variant);
-  public abstract int getNumCubitVariants(int[] numLayers);
-  public abstract float[][] getCubitPositions(int[] numLayers);
-  public abstract Static4D getCubitQuats(int cubit, int[] numLayers);
   public abstract float getScreenRatio();
   public abstract int[] getColorTable();
   public abstract String getShortName();
   public abstract String printColor(int color);
-
-  // not only for JSON
-  public abstract Static3D[] getRotationAxis();
-  public abstract int[][] getBasicAngles();
   public abstract int getNumPuzzleFaces();
   public abstract int getFOV();
   public abstract String[][] getTutorials();
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObjectOld.java b/src/main/java/org/distorted/objectlib/main/TwistyObjectOld.java
new file mode 100644
index 00000000..37fb9fba
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObjectOld.java
@@ -0,0 +1,1743 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlib.main;
+
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_BALL;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_BARREL;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_CHANGING_MIRROR;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_CHANGING_SHAPEMOD;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_CHANGING_SQUARE;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_CUBOID;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_DODECAHEDRON;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_HEXAHEDRON;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_ICOSAHEDRON;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_OCTAHEDRON;
+import static org.distorted.objectlib.touchcontrol.TouchControl.TC_TETRAHEDRON;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+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.VertexEffectSink;
+import org.distorted.library.helpers.QuatHelper;
+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.message.EffectListener;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.objectlib.helpers.FactoryCubit;
+import org.distorted.objectlib.helpers.FactorySticker;
+import org.distorted.objectlib.helpers.ObjectFaceShape;
+import org.distorted.objectlib.helpers.ObjectLibInterface;
+import org.distorted.objectlib.helpers.ObjectShape;
+import org.distorted.objectlib.helpers.ObjectSticker;
+import org.distorted.objectlib.helpers.ObjectStickerOverride;
+import org.distorted.objectlib.helpers.ObjectVertexEffects;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+import org.distorted.objectlib.json.JsonReader;
+import org.distorted.objectlib.metadata.Metadata;
+import org.distorted.objectlib.scrambling.ObjectScrambler;
+import org.distorted.objectlib.scrambling.ScrambleEdgeGenerator;
+import org.distorted.objectlib.signature.ObjectSignature;
+import org.distorted.objectlib.tablebases.ImplementedTablebasesList;
+import org.distorted.objectlib.tablebases.TablebasesAbstract;
+import org.distorted.objectlib.touchcontrol.TouchControl;
+import org.distorted.objectlib.touchcontrol.TouchControlBall;
+import org.distorted.objectlib.touchcontrol.TouchControlBarrel;
+import org.distorted.objectlib.touchcontrol.TouchControlCuboids;
+import org.distorted.objectlib.touchcontrol.TouchControlDodecahedron;
+import org.distorted.objectlib.touchcontrol.TouchControlHexahedron;
+import org.distorted.objectlib.touchcontrol.TouchControlIcosahedron;
+import org.distorted.objectlib.touchcontrol.TouchControlMirror;
+import org.distorted.objectlib.touchcontrol.TouchControlOctahedron;
+import org.distorted.objectlib.touchcontrol.TouchControlShapemod;
+import org.distorted.objectlib.touchcontrol.TouchControlSquare;
+import org.distorted.objectlib.touchcontrol.TouchControlTetrahedron;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class TwistyObjectOld extends TwistyObjectTheoretical
+  {
+/*
+  public static final int MODE_ICON = 0;
+  public static final int MODE_NORM = 1;
+
+  public static final int COLOR_STROKE   = 0xff000000;
+  public static final int COLOR_INTERNAL = 0xff000000;
+
+  private static final int DEFAULT_TEXTURE_HEIGHT = 256;
+  private static final int DEFAULT_TEXTURE_ROWS   = 8;
+
+  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 MAX_SIZE_CHANGE = 1.35f;
+  private static final float MIN_SIZE_CHANGE = 0.75f;
+
+  private static final Static3D CENTER = new Static3D(0,0,0);
+
+  protected float[][][][] mStickerCoords;
+
+  private int[][] mStickerVariants;
+  private float[] mStickerScales;
+  private Static4D[] mOrigQuat;
+  private TwistyObjectSolved mSolved;
+  private MeshBase[] mMeshes;
+  private int mNumFaceColors, mNumTextures, mNumOverrides;
+  private int mNumCubitFaces, mNumStickerTypes;
+  private Static4D[] mMixupModeQuats;
+  private boolean mIsInMixupMode;
+  private Static4D mQuat;
+  private int[] mNumLayers;
+  private float mSize;
+  private DistortedEffects mEffects;
+  private Static3D mObjectScale;
+  private DistortedTexture mTexture;
+  private float mInitScreenRatio;
+  private float mObjectScreenRatio;
+  private int mNumTexRows, mNumTexCols, mTexHeight;
+  private MeshBase mMesh;
+  private ObjectScrambler mScrambler;
+  private TouchControl mTouchControl;
+  private DistortedNode mNode;
+  private ObjectLibInterface mInterface;
+  private Bitmap mBitmap;
+  private ObjectSticker[] mStickers;
+  private ObjectShape[] mShapes;
+  private int mNumCubitVariants;
+  private int[][] mCubitFaceColors;
+  private int[][] mVariantFaceIsOuter;
+  private int mIconMode;
+  private Metadata mMetadata;
+  private ObjectStickerOverride[] mStickerOverrides;
+  private boolean mError;
+  private String mErrorString;
+  private int mMaxNumLayers;
+  private TwistyLayerRotations mRotation;
+  private int[] mColorTable;
+  private int[] mOriginalColorTable;
+  private float mTextureBorderMultiplier, mTextureCornerMultiplier;
+  private boolean mCurrentColorSchemeSubmittable;
+  private int[] mCubitQuats;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyObjectOld(int iconMode, Static4D quat, Static3D move, float scale, InitAssets asset)
+    {
+    try
+      {
+      InputStream jsonStream = asset!=null ? asset.getJsonStream(): null;
+      JsonReader reader = new JsonReader();
+      reader.parseJsonFile(jsonStream);
+      setReader(reader);
+      mNumLayers = reader.getNumLayers();
+      mSize      = reader.getSize();
+      mMetadata  = null;
+      initialize(iconMode,quat,move,scale,asset,true);
+      if( asset!=null ) asset.close();
+      mError = false;
+      mErrorString=null;
+      }
+    catch(Exception ex)
+      {
+      mError = true;
+      mErrorString = ex.getMessage();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public TwistyObjectOld(int iconMode, float size, Static4D quat, Static3D move, float scale, Metadata meta, InitAssets asset)
+    {
+    mNumLayers = meta.getNumLayers();
+    mSize      = size;
+    mMetadata  = meta;
+    initialize(iconMode,quat,move,scale,asset,false);
+    if( asset!=null ) asset.close();
+    mError = false;
+    mErrorString = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void initialize(int iconMode, Static4D quat, Static3D move, float scale, InitAssets asset, boolean fromJSON)
+    {
+    Static3D[] axis = getRotationAxis();
+    int[][] angles = getBasicAngles();
+    Static3D[] faceAxis = getFaceAxis();
+    float[][] positions = getCubitPositions(mNumLayers);
+    float[][] cuts = getCuts(mNumLayers);
+    int[][] minCubits = getMinimalCubiesInRow();
+
+    int numCubits = positions.length;
+    float[][] offsets = new float[numCubits][];
+    int[] types = new int[numCubits];
+
+    for(int c=0; c<numCubits; c++)
+      {
+      offsets[c] = getCubitRowOffset(c);
+      types[c] = getCubitRotationType(c);
+      }
+
+    super.initialize(axis, angles, positions, cuts, minCubits, offsets, types, faceAxis);
+
+    mIconMode = iconMode;
+    mQuat = quat;
+    mInitScreenRatio = getScreenRatio();
+    mCubitQuats = new int[mNumCubits];
+
+    mMaxNumLayers = -1;
+    for(int i=0; i<mNumAxis; i++)
+      {
+      if( mMaxNumLayers<mNumLayers[i] ) mMaxNumLayers = mNumLayers[i];
+      }
+
+    mOriginalColorTable = getColorTable();
+    mNumFaceColors = mOriginalColorTable.length;
+    mColorTable = new int[mNumFaceColors];
+    recreateFaceColors();
+    mTextureBorderMultiplier = 1.0f;
+    mTextureCornerMultiplier = 1.0f;
+
+    int scramblingType = getScrambleType();
+    int[][] edges = getScrambleEdges();
+    int[][] algorithms = getScrambleAlgorithms();
+
+    OperatingSystemInterface os = asset==null ? null : asset.getOS();
+    TablebasesAbstract tablebase = os!=null ? getTablebase(os) : null;
+    mScrambler = new ObjectScrambler(scramblingType,mNumAxis,mNumLayers,algorithms,edges,tablebase);
+
+    mObjectScale = new Static3D(scale,scale,scale);
+    setObjectRatioNow(scale,720);
+
+    mEffects = new DistortedEffects();
+    createQuaternionEffects();
+
+    mRotation = new TwistyLayerRotations(this,mAxis,mNumLayers,getGhostAngle(),mEffects);
+
+    MatrixEffectScale scaleEffect = new MatrixEffectScale(mObjectScale);
+    MatrixEffectQuaternion quatEffect = new MatrixEffectQuaternion(mQuat, CENTER);
+    MatrixEffectMove moveEffect = new MatrixEffectMove(move);
+
+    InputStream meshStream = asset!=null ? asset.getMeshStream(): null;
+    boolean fromDMESH = (meshStream!=null);
+    getQuatsAndShapes(fromDMESH,fromJSON);
+    createMesh(meshStream,fromDMESH);
+    setUpTextures(fromDMESH,fromJSON);
+
+    int index = getSolvedFunctionIndex();
+    mSolved = new TwistyObjectSolved(this,mCubitPos,index);
+    mSolved.setupSolvedQuats(getSolvedQuats());
+    mSolved.setPuzzleFaceColor(mColorTable);
+
+    mEffects.apply(quatEffect);
+    mEffects.apply(scaleEffect);
+    mEffects.apply(moveEffect);
+
+    mNode = new DistortedNode(mTexture,mEffects,mMesh);
+
+    for(int c=0; c<mNumCubits; c++) setCubitQuat(c,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void recreateFaceColors()
+    {
+    for(int i=0; i<mNumFaceColors; i++) mColorTable[i] = mOriginalColorTable[i];
+    mCurrentColorSchemeSubmittable = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// in degrees so that everything can be treated modulo 360
+
+  public int getGhostAngle()
+    {
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private TablebasesAbstract getTablebase(OperatingSystemInterface os)
+    {
+    String shortName = getShortName();
+    return ImplementedTablebasesList.createPacked(os,shortName);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createQuaternionEffects()
+    {
+    if( mNumQuats<=ObjectControl.MAX_QUATS )
+      {
+      mIsInMixupMode = false;
+
+      for( int q=0; q<mNumQuats; q++)
+        {
+        VertexEffectQuaternion vq = new VertexEffectQuaternion(mObjectQuats[q],CENTER);
+        vq.setMeshAssociation(0,q);
+        mEffects.apply(vq);
+        }
+      }
+    else if( mNumCubits<=ObjectControl.MAX_QUATS )
+      {
+      mIsInMixupMode = true;
+      mMixupModeQuats = new Static4D[mNumCubits];
+
+      for( int q=0; q<mNumCubits; q++)
+        {
+        mMixupModeQuats[q] = new Static4D(mObjectQuats[0]);
+        VertexEffectQuaternion vq = new VertexEffectQuaternion(mMixupModeQuats[q],CENTER);
+        vq.setMeshAssociation(0,q);
+        mEffects.apply(vq);
+        }
+      }
+    else
+      {
+      android.util.Log.e("D", "object has too many quaternions ("+mNumQuats+") or too many cubits ("+mNumCubits+")");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 createOuterFaces()
+    {
+    for(int v=0; v<mNumCubitVariants; v++)
+      {
+      int numFaces = mShapes[v].getNumFaces();
+      mVariantFaceIsOuter[v] = new int[numFaces];
+      }
+
+    for( int cubit=0; cubit<mNumCubits; cubit++)
+      {
+      int variant = getCubitVariant(cubit,mNumLayers);
+      int numFaces = mShapes[variant].getNumFaces();
+
+      for(int face=0; face<numFaces; face++)
+        if( getDefaultCubitFaceColor(cubit,face)>=0 )
+          {
+          mVariantFaceIsOuter[variant][face] = 1;
+          }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void getQuatsAndShapes(boolean fromDMESH, boolean fromJSON)
+    {
+    mNumCubitVariants = getNumCubitVariants(mNumLayers);
+
+    if( !fromDMESH || !fromJSON )
+      {
+      FactoryCubit factory = FactoryCubit.getInstance();
+      factory.clear();
+
+      mShapes = new ObjectShape[mNumCubitVariants];
+      for(int v=0; v<mNumCubitVariants; v++) mShapes[v] = getObjectShape(v);
+      mNumCubitFaces = ObjectShape.computeNumComponents(mShapes);
+      mVariantFaceIsOuter = new int[mNumCubitVariants][];
+
+      mOrigQuat = new Static4D[mNumCubits];
+      for(int i=0; i<mNumCubits; i++) mOrigQuat[i] = getCubitQuats(i,mNumLayers);
+
+      if( !fromJSON )
+        {
+        mCubitFaceColors = ObjectShape.computeColors(mShapes,mCubitPos,mOrigQuat,this);
+        createOuterFaces();
+        }
+
+      if( fromDMESH )
+        {
+        for(int i=0; i<mNumCubitVariants; i++) factory.createNewFaceTransform(mShapes[i], mVariantFaceIsOuter[i]);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createMesh(InputStream stream, boolean fromDMESH)
+    {
+    if( fromDMESH )
+      {
+      DataInputStream dos = new DataInputStream(stream);
+      mMesh = new MeshFile(dos);
+
+      try
+        {
+        stream.close();
+        }
+      catch(IOException e)
+        {
+        android.util.Log.e("meshFile", "Error closing InputStream: "+e);
+        }
+      }
+    else
+      {
+      MeshBase[] cubitMesh = new MeshBase[mNumCubits];
+
+      for(int i=0; i<mNumCubits; i++)
+        {
+        cubitMesh[i] = createCubitMesh(i,mNumLayers,mNumCubitFaces);
+        Static3D pos = getPos(mCubitPos[i]);
+        cubitMesh[i].apply(new MatrixEffectMove(pos),1,0);
+        }
+
+      mMesh = new MeshJoined(cubitMesh);
+
+      float pillowCoeff = getPillowCoeff();
+
+      if( pillowCoeff!=1.0f )
+        {
+        float radius = getCircumscribedRadius();
+        Static1D coeff = new Static1D(pillowCoeff);
+        Static4D region= new Static4D(0,0,0,radius);
+        VertexEffectSink sink = new VertexEffectSink(coeff,CENTER,region);
+        mMesh.apply(sink);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private MeshBase createCubitMesh(int cubit, int[] numLayers, int numComponents)
+    {
+    int variant = getCubitVariant(cubit,numLayers);
+
+    if( mMeshes==null ) mMeshes = new MeshBase[mNumCubitVariants];
+
+    if( mMeshes[variant]==null )
+      {
+      ObjectFaceShape faceShape = getObjectFaceShape(variant);
+      ObjectVertexEffects effects = getVertexEffects(variant);
+      FactoryCubit factory = FactoryCubit.getInstance();
+      factory.createNewFaceTransform(mShapes[variant],mVariantFaceIsOuter[variant]);
+      mMeshes[variant] = factory.createRoundedSolid(mShapes[variant],faceShape,effects,numComponents);
+      }
+
+    MeshBase mesh = mMeshes[variant].copy(true);
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( mOrigQuat[cubit], CENTER );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void figureOutBitmapDimensions(int numTextures)
+    {
+    int maxSize = DistortedLibrary.getMaxTextureSize();
+
+    mTexHeight  = DEFAULT_TEXTURE_HEIGHT;
+
+    while(true)
+      {
+      mNumTexCols = DEFAULT_TEXTURE_ROWS;
+      mNumTexRows = numTextures/mNumTexCols + 1;
+
+      if( mNumTexRows*mTexHeight <= maxSize &&
+          mNumTexCols*mTexHeight <= maxSize  ) break;
+
+      mTexHeight/=2;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setUpTextures(boolean fromDMESH, boolean fromJSON)
+    {
+    mTexture = new DistortedTexture();
+
+    if( fromJSON )
+      {
+      mNumStickerTypes = getNumStickerTypes();
+      mNumCubitFaces = getNumCubitFaces();
+      }
+    else
+      {
+      FactoryCubit factory = FactoryCubit.getInstance();
+      mStickerCoords   = factory.getStickerCoords();
+      mStickerVariants = factory.getStickerVariants();
+      mStickerScales   = factory.getStickerScales();
+      adjustStickerCoords();
+      mNumStickerTypes = (mStickerCoords==null ? 0 : mStickerCoords.length);
+      }
+
+    mStickerOverrides = getStickerOverrides();
+    mNumOverrides = mStickerOverrides==null ? 0 : mStickerOverrides.length;
+
+    mNumTextures= mNumFaceColors*mNumStickerTypes + mNumOverrides;
+    figureOutBitmapDimensions(mNumTextures);
+    if( mNumTexCols*mNumTexRows < mNumTextures+1 ) mNumTexRows++;
+
+    if( !fromDMESH || shouldResetTextureMaps() ) resetAllTextureMaps();
+    else overrideCubitFaceColor();
+
+    setTexture();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Metadata getMetadata()
+    {
+    return mMetadata;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isInIconMode()
+    {
+    return mIconMode==MODE_ICON;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getVariantStickerShape(int variant, int face)
+    {
+    if( variant <mStickerVariants.length )
+      {
+      int[] var = mStickerVariants[variant];
+      return face>=var.length ? -1 : var[face];
+      }
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean shouldResetTextureMaps()
+    {
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getScrambleAlgorithms()
+    {
+    return ScrambleEdgeGenerator.getScramblingAlgorithms(mBasicAngles);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void rotateAllCubits(int axisIndex, int rowBitmap, int angle)
+    {
+    super.rotateAllCubits(axisIndex,rowBitmap,angle);
+    for(int i=0; i<mNumCubits; i++) setCubitQuat(i,getCubitQuatIndex(i));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean sticksOut(Static3D[] faceAxis, float[] dist, float x, float y, float z )
+    {
+    final float MAXERR = 0.05f;
+    int numAxis = dist.length;
+
+    for(int i=0; i<numAxis; i++)
+      {
+      Static3D ax = faceAxis[i];
+      float len = ax.get0()*x + ax.get1()*y + ax.get2()*z;
+      if( len>mSize*dist[i]+MAXERR ) return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean doesNotStickOut(int variant, float px, float py, float pz, float[] tmp, Static4D quat)
+    {
+    float[][] vertices = mShapes[variant].getVertices();
+    Static3D[] axis = getFaceAxis();
+    float[] dist3D = getDist3D(mNumLayers);
+
+    for( float[] vertex : vertices)
+      {
+      float x = vertex[0];
+      float y = vertex[1];
+      float z = vertex[2];
+
+      QuatHelper.rotateVectorByQuat(tmp, x, y, z, 1, quat);
+
+      float mx = tmp[0] + px;
+      float my = tmp[1] + py;
+      float mz = tmp[2] + pz;
+
+      if( sticksOut(axis, dist3D, mx,my,mz) ) return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float computeAvg(float[] pos, int offset)
+    {
+    int len = pos.length/3;
+    float ret=0.0f;
+    for(int i=0; i<len; i++) ret += pos[3*i+offset];
+    ret /= len;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected void displayCubitQuats()
+    {
+    StringBuilder builder = new StringBuilder();
+    float[] tmp = new float[4];
+    float ERR = 0.01f;
+
+    for(int cubit=0; cubit<mNumCubits; cubit++)
+      {
+      builder.append(cubit);
+      builder.append(" : ");
+
+      int refCubit,variant = getCubitVariant(cubit,mNumLayers);
+
+      for(refCubit=0; refCubit<mNumCubits; refCubit++)
+        {
+        if( getCubitVariant(refCubit,mNumLayers)==variant ) break;
+        }
+
+      float[] curpos = mCubitPos[cubit];
+      float[] refpos = mCubitPos[refCubit];
+      float refX = computeAvg(refpos,0);
+      float refY = computeAvg(refpos,1);
+      float refZ = computeAvg(refpos,2);
+      float curX = computeAvg(curpos,0);
+      float curY = computeAvg(curpos,1);
+      float curZ = computeAvg(curpos,2);
+
+      for(int quat=0; quat<mNumQuats; quat++)
+        {
+        QuatHelper.rotateVectorByQuat(tmp,refX,refY,refZ,0,mObjectQuats[quat]);
+
+        float dx = tmp[0]-curX;
+        float dy = tmp[1]-curY;
+        float dz = tmp[2]-curZ;
+
+        if( dx>-ERR && dx<ERR && dy>-ERR && dy<ERR && dz>-ERR && dz<ERR )
+          {
+          if( doesNotStickOut(variant,curX,curY,curZ,tmp,mObjectQuats[quat]) )
+            {
+            builder.append(quat);
+            builder.append(',');
+            }
+          else
+            {
+            android.util.Log.e("D", "cubit: "+cubit+" quat: "+quat+" : center correct, but shape sticks out");
+            }
+          }
+        }
+
+      builder.append('\n');
+      }
+
+    android.util.Log.e("D", "cubitQuats: \n"+builder );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitRotationType(int cubit)
+    {
+    return TwistyObjectTheoreticalCubit.TYPE_NORMAL;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getCubitRowOffset(int cubitIndex)
+    {
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getSolvedQuats()
+    {
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected int[][] getOldSolvedQuats()
+    {
+    return mSolved.getSolvedQuats(mNumLayers,mNumCubitFaces,mCubitFaceColors);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 0: old, groups-of-quaternions based function.
+// Still used by: Masterball, Mirror Skewb, Mirror Jing, Mirror Pyraminx & the Penrose Cubes.
+//
+// 1: specific function only for the Dino4.
+//
+// 2: the new default; automatic way based on automatic detection of monochromatic puzzle surfaces.
+
+  public int getSolvedFunctionIndex()
+    {
+    return 2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getScrambleType()
+    {
+    return ObjectScrambler.SCRAMBLING_ALGORITHMS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getMinimalCubiesInRow()
+    {
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setLibInterface(ObjectLibInterface inter)
+    {
+    mInterface = inter;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized float removeRotation()
+    {
+    return mRotation.removeRotation();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  long finishRotation(ObjectPreRender pre, int finishAx, int finishRow, float finishAngle, float avgSpeed)
+    {
+    int basicAngle = mBasicAngles[finishAx][finishRow];
+    return mRotation.finishRotation(pre,finishAx,finishRow,basicAngle,finishAngle,avgSpeed);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void continueRotation(float angleInDegrees)
+    {
+    mRotation.continueRotation(angleInDegrees);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized long addRotation(EffectListener listener, int axis, int rowBitmap, int angle, long durationMillis )
+    {
+    return mRotation.addRotation(listener,axis,rowBitmap,angle,durationMillis);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enableGhostAxis(int axNum, boolean enable)
+    {
+    mTouchControl.enableGhostAxis(axNum,enable);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized boolean beginRotation(int axis, int row )
+    {
+    if( axis<0 || axis>=mNumAxis )
+      {
+      android.util.Log.e("object", "invalid rotation axis: "+axis);
+      return false;
+      }
+
+    return mRotation.beginRotation(axis,row);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setTextureMap(int cubit, int face, int color)
+    {
+    int variant  = getCubitVariant(cubit,mNumLayers);
+    int shape    = getVariantStickerShape(variant,face);
+    int texIndex = color<0 || shape<0 ? mNumTextures-mNumOverrides : shape*mNumFaceColors + color;
+    int row      = (mNumTexRows-1) - texIndex/mNumTexCols;
+    int col      = texIndex%mNumTexCols;
+
+    final float ratioW = 1.0f/mNumTexCols;
+    final float ratioH = 1.0f/mNumTexRows;
+    final Static4D[] maps = new Static4D[1];
+    maps[0] = new Static4D(col*ratioW, row*ratioH, ratioW, ratioH);
+    mMesh.setTextureMap(maps,mNumCubitFaces*cubit+face);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// figure out the whole face to which (cubit,face) belongs, repaint all (cubit,face) pairs to
+// the new color.
+
+  void repaintPuzzleFace(int cubit, int face, int newColor)
+    {
+    int oldColorIndex = getDefaultCubitFaceColor(cubit,face);
+
+    if( oldColorIndex<0 || oldColorIndex>=mNumFaceColors )
+      {
+      android.util.Log.e("D", "TwistyObject.repaintPuzzleFace: index="+oldColorIndex);
+      return;
+      }
+
+    int oldColor = mColorTable[oldColorIndex];
+
+    if( oldColor!=newColor )
+      {
+      oldColor = mOriginalColorTable[oldColorIndex];
+      changeColorInTexture(oldColor,newColor);
+      mColorTable[oldColorIndex] = newColor;
+      setTexture();
+
+      int numOrig = numberOfDifferentColors(mOriginalColorTable);
+      int numNow  = numberOfDifferentColors(mColorTable);
+      mCurrentColorSchemeSubmittable = (numOrig==numNow);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this doesn't have to return the real, displayed color - but the default one (from Shape classes).
+// The real displayed color can be different because of the sticker color overrides in 'Config'.
+
+  int getDefaultCubitFaceColor(int cubit, int face)
+    {
+    int puzzleFace = getCubitFaceMap(cubit,face);
+    if( puzzleFace>=0 ) puzzleFace %= mNumFaceColors;
+    return puzzleFace;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitFaceMap(int cubit, int face)
+    {
+    int numFaces = mCubitFaceColors[cubit].length;
+    int puzzleFace = face<numFaces ? mCubitFaceColors[cubit][face] : -1;
+    return puzzleFace<0 ? -1 : puzzleFace;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this would return the REAL value of outer; the 'getCubitFaceColor()>=0' returns if the face is
+// colored, which for example in case of Container (which has colored internal faces) is not the
+// same thing
+
+  boolean faceIsOuter(int cubit, int face)
+    {
+    if( mCubitFaceColors==null )
+      {
+      if( mShapes==null )
+        {
+        mShapes = new ObjectShape[mNumCubitVariants];
+        }
+
+      if( mShapes[0]==null )
+        {
+        for(int v=0; v<mNumCubitVariants; v++) mShapes[v] = getObjectShape(v);
+        }
+
+      if( mOrigQuat==null )
+        {
+        mOrigQuat = new Static4D[mNumCubits];
+        }
+
+      if( mOrigQuat[0]==null )
+        {
+        for(int c=0; c<mNumCubits; c++) mOrigQuat[c] = getCubitQuats(c,mNumLayers);
+        }
+
+      mCubitFaceColors = ObjectShape.computeColors(mShapes,mCubitPos,mOrigQuat,this);
+      }
+
+    return mCubitFaceColors[cubit][face]>=0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void resetAllTextureMaps()
+    {
+    final float ratioW = 1.0f/mNumTexCols;
+    final float ratioH = 1.0f/mNumTexRows;
+    int cubColor, stiShape, texIndex, variant, row, col;
+
+    for(int cubit=0; cubit<mNumCubits; cubit++)
+      {
+      final Static4D[] maps = new Static4D[mNumCubitFaces];
+      variant = getCubitVariant(cubit,mNumLayers);
+
+      for(int face=0; face<mNumCubitFaces; face++)
+        {
+        cubColor = getDefaultCubitFaceColor(cubit,face);
+        stiShape = getVariantStickerShape(variant,face);
+        texIndex = cubColor<0 || stiShape<0 ? mNumTextures-mNumOverrides : stiShape*mNumFaceColors + cubColor;
+        row      = (mNumTexRows-1) - texIndex/mNumTexCols;
+        col      = texIndex%mNumTexCols;
+
+        maps[face] = new Static4D( col*ratioW, row*ratioH, ratioW, ratioH);
+        }
+
+      mMesh.setTextureMap(maps,mNumCubitFaces*cubit);
+      }
+
+    overrideCubitFaceColor();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void overrideCubitFaceColor()
+    {
+    final float ratioW = 1.0f/mNumTexCols;
+    final float ratioH = 1.0f/mNumTexRows;
+
+    for(int i=0; i<mNumOverrides; i++)
+      {
+      int[] cubitFaces = mStickerOverrides[i].getCubitFaces();
+      int length = cubitFaces.length/2;
+
+      for(int j=0; j<length; j++)
+        {
+        final Static4D[] maps = new Static4D[1];
+        int color = mNumTextures-mNumOverrides+1+i;
+        int row   = (mNumTexRows-1) - color/mNumTexCols;
+        int col   = color%mNumTexCols;
+        int cubit = cubitFaces[2*j];
+        int face  = cubitFaces[2*j+1];
+        maps[0] = new Static4D(col*ratioW, row*ratioH, ratioW, ratioH);
+        mMesh.setTextureMap(maps,mNumCubitFaces*cubit+face);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void releaseResources()
+    {
+    super.releaseResources();
+
+    mTexture.markForDeletion();
+    mMesh.markForDeletion();
+    mEffects.markForDeletion();
+
+    if( mBitmap!=null )
+      {
+      mBitmap.recycle();
+      mBitmap = null;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int computeAssociation(int cubit)
+    {
+    int result = 0, accumulativeShift = 0;
+
+    for(int axis=0; axis<mNumAxis; axis++)
+      {
+      int rotRow = getCubitRotRow(cubit,axis);
+      result += (rotRow<<accumulativeShift);
+      accumulativeShift += mMaxNumLayers;
+      }
+
+    return result;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setCubitQuat(int cubit, int equAssociation)
+    {
+    int andAssociation = computeAssociation(cubit);
+
+    if( !mIsInMixupMode )
+      {
+      mMesh.setEffectAssociation(cubit,andAssociation,equAssociation);
+      }
+    else
+      {
+      mMesh.setEffectAssociation(cubit,andAssociation,cubit);
+      Static4D tmp = mObjectQuats[equAssociation];
+      mMixupModeQuats[cubit].set(tmp);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int numberOfDifferentColors(int[] table)
+    {
+    int ret=0;
+    int len = table.length;
+
+    for(int i=0; i<len; i++)
+      {
+      boolean increase = true;
+
+      for(int j=0; j<i; j++)
+        if( table[j]==table[i] )
+          {
+          increase = false;
+          break;
+          }
+
+      if( increase ) ret++;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void restoreSti(String key, OperatingSystemInterface os)
+    {
+    mTextureBorderMultiplier = os.getFloat(key+"_border", 1.0f);
+    mTextureCornerMultiplier = os.getFloat(key+"_corner", 1.0f);
+    String colors = os.getString(key+"_colors", null);
+    boolean different = false;
+
+    if( colors!=null ) different = restoreColors(colors,mColorTable);
+
+    if( different || mTextureBorderMultiplier!=1.0f || mTextureCornerMultiplier!=1.0f )
+      {
+      createTexture(mTextureBorderMultiplier,mTextureCornerMultiplier);
+      setTexture();
+      mSolved.setPuzzleFaceColor(mColorTable);
+
+      int numOrig = numberOfDifferentColors(mOriginalColorTable);
+      int numNow  = numberOfDifferentColors(mColorTable);
+      mCurrentColorSchemeSubmittable = (numOrig==numNow);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void restorePreferences(OperatingSystemInterface os)
+    {
+    String key = getShortName();
+    boolean error = super.restorePreferences(key,os);
+
+    if( error )
+      {
+      for(int c=0; c<mNumCubits; c++) setCubitQuat(c,0);
+      }
+    else
+      {
+      for(int c=0; c<mNumCubits; c++)
+        {
+        int q = getCubitQuatIndex(c);
+        setCubitQuat(c, q);
+        }
+      }
+
+    restoreSti(key,os);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void restoreStickers(OperatingSystemInterface os)
+    {
+    String key = getShortName();
+    restoreSti(key,os);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void savePreferences(OperatingSystemInterface os)
+    {
+    String key = getShortName();
+    super.savePreferences(key,os);
+
+    os.putFloat(key+"_border", mTextureBorderMultiplier);
+    os.putFloat(key+"_corner", mTextureCornerMultiplier);
+    os.putString(key+"_colors", createColors(mColorTable) );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void removePreferences(OperatingSystemInterface os)
+    {
+    String key = getShortName();
+    super.removePreferences(key,os);
+
+    os.remove(key+"_border");
+    os.remove(key+"_corner");
+    os.remove(key+"_colors");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String createColors(int[] table)
+    {
+    StringBuilder sb = new StringBuilder();
+    int len = table!=null ? table.length : 0;
+
+    for(int i=0; i<len; i++)
+      {
+      sb.append(table[i]);
+      sb.append(',');
+      }
+
+    return sb.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean restoreColors(String colors, int[] table)
+    {
+    String[] parts = colors.split(",");
+    int len = parts.length;
+    boolean ret = false;
+
+    try
+      {
+      for(int s=0; s<len; s++)
+        {
+        table[s] = Integer.parseInt(parts[s]);
+        if( table[s]!=mOriginalColorTable[s] ) ret = true;
+        }
+      }
+    catch(NumberFormatException nfe)
+      {
+      for(int s=0; s<len; s++) table[s] = mOriginalColorTable[s];
+      return false;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float computeRadiusCorrection(float[][] sticker, int curr, int len)
+    {
+    final float A = 0.8f;  // 0<A<1
+
+    int prev = curr>0 ? curr-1 : len-1;
+    int next = curr<len-1 ? curr+1 : 0;
+
+    float v1x = sticker[prev][0]-sticker[curr][0];
+    float v1y = sticker[prev][1]-sticker[curr][1];
+    float v2x = sticker[next][0]-sticker[curr][0];
+    float v2y = sticker[next][1]-sticker[curr][1];
+
+    float len1= v1x*v1x+v1y*v1y;
+    float len2= v2x*v2x+v2y*v2y;
+
+    float cos = (v1x*v2x+v1y*v2y) / ( (float)Math.sqrt(len1*len2) );
+
+    return 1-A*cos;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Radius of the sphere circumscribed on the puzzle. Needed for pillowing.
+//
+// This won't work correctly for pillowing off-center puzzles (e.g. mirrors) - for those we'd need
+// to introduce the concept of a 'sink center' as well.
+//
+// public because needed in TouchControlShapemod
+
+  public float getCircumscribedRadius()
+    {
+    switch( getCategory() )
+      {
+      case Metadata.CATEGORY_SHAPE_TET: return (SQ6/4)*mSize;
+      case Metadata.CATEGORY_SHAPE_HEX: return (SQ3/2)*mSize;
+      case Metadata.CATEGORY_SHAPE_OCT: return (SQ2/2)*mSize;
+      case Metadata.CATEGORY_SHAPE_DOD: return (SQ3/2)*((SQ5+1)/2)*mSize;
+      case Metadata.CATEGORY_SHAPE_BAL: return 0.50f*mSize;
+      case Metadata.CATEGORY_SHAPE_ICO: return 0; // no pillowed icosahedra
+      case Metadata.CATEGORY_SHAPE_BAR: return 0; // no pillowed barrels
+      }
+
+    return 0.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// might be overridden in subclasses which want per-edge radii
+
+  protected float[][][] getStickerRadii()
+    {
+    float radius = getStickerRadius();
+    int numStickers = mStickerCoords.length;
+    float[][][] ret = new float[numStickers][][];
+
+    for(int s=0; s<numStickers; s++)
+      {
+      int numLoops = mStickerCoords[s].length;
+      ret[s] = new float[numLoops][];
+
+      for(int l=0; l<numLoops; l++)
+        {
+        int numVertices = mStickerCoords[s][l].length;
+        ret[s][l] = new float[numVertices];
+        for(int v=0; v<numVertices; v++) ret[s][l][v] = radius;
+        }
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// might be overridden in subclasses which want per-edge strokes
+
+  protected float[][][] getStickerStrokes()
+    {
+    float stroke = getStickerStroke();
+    int numStickers = mStickerCoords.length;
+    float[][][] ret = new float[numStickers][][];
+
+    for(int s=0; s<numStickers; s++)
+      {
+      int numLoops = mStickerCoords[s].length;
+      ret[s] = new float[numLoops][];
+
+      for(int l=0; l<numLoops; l++)
+        {
+        int numVertices = mStickerCoords[s][l].length;
+        ret[s][l] = new float[numVertices];
+        for(int v=0; v<numVertices; v++) ret[s][l][v] = stroke;
+        }
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectSticker retSticker(int sticker)
+    {
+    if( mStickers==null )
+      {
+      float[][][] radii   = getStickerRadii();
+      float[][][] strokes = getStickerStrokes();
+      float[][][] angles  = getStickerAngles();
+      int numStickers = mStickerCoords.length;
+      mStickers = new ObjectSticker[numStickers];
+
+      for(int s=0; s<numStickers; s++)
+        {
+        float scale = mStickerScales.length>s ? mStickerScales[s] : 1.0f;
+        int numLoops = mStickerCoords[s].length;
+        float[][] rad = new float[numLoops][];
+        float[][] str = new float[numLoops][];
+
+        for(int l=0; l<numLoops; l++)
+          {
+          int numVerts = mStickerCoords[s][l].length;
+          rad[l] = new float[numVerts];
+          str[l] = new float[numVerts];
+          float[] st = strokes[s][l];
+          float[] ra = radii[s][l];
+
+          for(int v=0; v<numVerts; v++)
+            {
+            rad[l][v] = ra[v] * computeRadiusCorrection(mStickerCoords[s][l],v,numVerts) / scale;
+            str[l][v] = st[v] / scale;
+            }
+          }
+
+        mStickers[s] = new ObjectSticker(mStickerCoords[s], (angles==null ? null : angles[s]) ,rad, str);
+        }
+      }
+
+    return mStickers[sticker];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// some objects (currently Kilominx,Ivy,Rex) might want to change the stickers.
+
+  public void adjustStickerCoords()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Return the 'surface' the given face of the given cubit is on. (px,py,pz) is the pre-computed
+// center of the cubit (so we don't have to keep re-computing this per each face)
+// If the face is not external, return null.
+//
+// This is needed by the TwistyObjectSolved's method2 detecting if the object is in a solved state.
+//
+// What is a 'surface' ? For objects with flat monochromatic external walls, it is simply a 4-tuple
+// [(nx,ny,nz),d] (normal vector + distance from origin) describing a 3D plane this face is part of.
+// Exceptions are objects with curved external walls (ATM Masterball & Penrose Cubes): it is a union
+// of several surfaces which together define the bent, monochromatic wall. I.e. Penrose Cube has 3
+// surfaces (blue, yellow and red), each defined by 3 planes (think before the bending effect!)
+//
+// This is the default which simply returns the single plane. If some puzzle has more complicated
+// surfaces, it needs to override this method.
+//
+// We never need to test if a given point is part of a given surface (see TwistyObjectSurface!) - we
+// only need to be able to rotate the surface by quats and test if two surfaces are the same. Thus:
+// it is possible to provide some artificial surfaces - for example, in case of the Penrose, the
+// three surfaces can be only the single 'middle' plane [the one on the bend].
+
+  public float[] getMonochromaticSurface(int variant, int cubitIndex, int faceIndex, float px, float py, float pz)
+    {
+    if( !faceIsOuter(cubitIndex,faceIndex) ) return null;
+
+    Static4D cubitQuat = getCubitQuats(cubitIndex,mNumLayers);
+
+    float[] normal   = new float[4];
+    float[] intPoint = new float[3];
+    float[] rotPoint = new float[4];
+    float[] surface  = new float[4];
+
+    ObjectShape objShape = mShapes[variant];
+    objShape.getFacePoint(faceIndex,intPoint);
+    objShape.getFaceNormal(faceIndex,normal);
+
+    QuatHelper.rotateVectorByQuat(rotPoint,intPoint[0],intPoint[1],intPoint[2],1,cubitQuat);
+    QuatHelper.rotateVectorByQuat(surface,normal[0],normal[1],normal[2],0,cubitQuat);
+
+    float x = rotPoint[0] + px;
+    float y = rotPoint[1] + py;
+    float z = rotPoint[2] + pz;
+    surface[3] = x*surface[0] + y*surface[1] + z*surface[2];
+
+    return surface;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getVariantFaceIsOuter()
+    {
+    return mVariantFaceIsOuter;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getInternalColor()
+    {
+    return COLOR_INTERNAL;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// the getFaceColors + final INTERNAL_COLOR in a grid (so that we do not exceed the maximum texture size)
+
+  void createTexture(float border, float corner)
+    {
+    Paint paint = new Paint();
+    Canvas canvas = new Canvas(mBitmap);
+
+    paint.setAntiAlias(true);
+    paint.setTextAlign(Paint.Align.CENTER);
+    paint.setStyle(Paint.Style.FILL);
+    paint.setColor(getInternalColor());
+    canvas.drawRect(0, 0, mNumTexCols*mTexHeight, mNumTexRows*mTexHeight, paint);
+
+    mTextureBorderMultiplier = border;
+    mTextureCornerMultiplier = corner;
+
+    int texture = 0;
+    FactorySticker factory = FactorySticker.getInstance();
+
+    for(int row=0; row<mNumTexRows; row++)
+      for(int col=0; col<mNumTexCols; col++)
+        {
+        if( texture<mNumTextures-mNumOverrides )
+          {
+          ObjectSticker sticker = retSticker(texture/mNumFaceColors);
+          int color = mColorTable[texture%mNumFaceColors];
+          factory.drawRoundedPolygons( canvas, paint, col*mTexHeight, (mNumTexRows-row)*mTexHeight, color,
+                                       mTexHeight, sticker,mTextureBorderMultiplier,mTextureCornerMultiplier);
+          }
+        else if( texture>mNumTextures-mNumOverrides && texture<=mNumTextures )
+          {
+          int color = mStickerOverrides[mNumTextures-texture].getColor();
+          factory.drawSolidColor(canvas, paint, col*mTexHeight, (mNumTexRows-row)*mTexHeight, color, mTexHeight);
+          }
+
+        texture++;
+        }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void changeColorInTexture(int oldColor, int newColor)
+    {
+    Paint paint = new Paint();
+    Canvas canvas = new Canvas(mBitmap);
+    paint.setAntiAlias(true);
+    int texture = 0;
+    FactorySticker factory = FactorySticker.getInstance();
+
+    for(int row=0; row<mNumTexRows; row++)
+      for(int col=0; col<mNumTexCols; col++)
+        {
+        if( texture<mNumTextures-mNumOverrides )
+          {
+          int color = mOriginalColorTable[texture%mNumFaceColors];
+
+          if( color==oldColor )
+            {
+            ObjectSticker sticker = retSticker(texture/mNumFaceColors);
+            factory.drawRoundedPolygons( canvas, paint, col*mTexHeight, (mNumTexRows-row)*mTexHeight,
+                                         newColor, mTexHeight, sticker,mTextureBorderMultiplier,mTextureCornerMultiplier);
+            }
+          }
+
+        texture++;
+        }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setTexture()
+    {
+    if( mBitmap==null )
+      {
+      mBitmap = Bitmap.createBitmap( mNumTexCols*mTexHeight, mNumTexRows*mTexHeight, Bitmap.Config.ARGB_4444);
+      createTexture(mTextureBorderMultiplier,mTextureCornerMultiplier);
+      }
+
+    if( !mTexture.setTextureAlreadyInverted(mBitmap) )
+      {
+      int max = DistortedLibrary.getMaxTextureSize();
+      mInterface.reportProblem("failed to set texture of size "+mBitmap.getWidth()+"x"+mBitmap.getHeight()+" max is "+max, true);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setObjectRatioNow(float sc, int nodeSize)
+    {
+    mObjectScreenRatio = sc;
+    float scale = mObjectScreenRatio*mInitScreenRatio*nodeSize/mSize;
+    mObjectScale.set(scale,scale,scale);
+
+    if( mTouchControl ==null ) mTouchControl = getTouchControl();
+    mTouchControl.setObjectRatio(mObjectScreenRatio*mInitScreenRatio);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setObjectRatio(float sizeChange, int nodeSize)
+    {
+    mObjectScreenRatio *= (1.0f+sizeChange)/2;
+
+    if( mObjectScreenRatio>MAX_SIZE_CHANGE) mObjectScreenRatio = MAX_SIZE_CHANGE;
+    if( mObjectScreenRatio<MIN_SIZE_CHANGE) mObjectScreenRatio = MIN_SIZE_CHANGE;
+
+    setObjectRatioNow(mObjectScreenRatio, nodeSize);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setNodeSize(int nodeSize)
+    {
+    setObjectRatioNow(mObjectScreenRatio, nodeSize);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getRatio()
+    {
+    return mObjectScreenRatio;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getObjectRatio()
+    {
+    return mObjectScreenRatio*mInitScreenRatio;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean isSolved()
+    {
+    for(int c=0; c<mNumCubits; c++) mCubitQuats[c] = getCubitQuatIndex(c);
+    boolean solved = mSolved.isSolved(mCubitQuats);
+    return mRotation.isSolved(solved);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// INTERNAL API - those are called from 'effects' package
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void randomizeNewScramble(int[][] scramble, Random rnd, int curr, int total)
+    {
+    mScrambler.randomizeNewScramble(scramble,rnd,curr,total, getSignature() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static4D getRotationQuat()
+    {
+    return mQuat;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getSize()
+    {
+    return mSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void applyEffect(Effect effect, int position)
+    {
+    mEffects.apply(effect, position);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void removeEffect(long effectID)
+    {
+    mEffects.abortById(effectID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public MeshBase getObjectMesh()
+    {
+    return mMesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public DistortedEffects getObjectEffects()
+    {
+    return mEffects;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectStickerOverride[] getStickerOverrides()
+    {
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean getError()
+    {
+    return mError;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getErrorString()
+    {
+    return mErrorString;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitFaceStickerIndex(int cubit, int face)
+    {
+    Static4D texMap = mMesh.getTextureMap(mNumCubitFaces*cubit + face);
+
+    int x = (int)(texMap.get0()/texMap.get2());
+    int y = (int)(texMap.get1()/texMap.get3());
+
+    return (mNumTexRows-1-y)*mNumTexCols + x;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getNumLayers()
+    {
+    return mNumLayers;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void solve()
+    {
+    super.solve();
+    for(int i=0; i<mNumCubits; i++) setCubitQuat(i,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Bitmap getStickerBitmap()
+    {
+    return mBitmap;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public DistortedNode getNode()
+    {
+    return mNode;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumStickerTypes()
+    {
+    return mNumStickerTypes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String reportState()
+    {
+    StringBuilder builder = new StringBuilder();
+
+    for(int i=0; i<mNumCubits; i++ )
+      {
+      if( i>0 ) builder.append('.');
+      builder.append(getCubitQuatIndex(i));
+      }
+
+    return builder.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this is here only so it can be overridden in TwistyJSON so that we can get this from JSON.
+
+  public int getNumCubitFaces()
+    {
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 1.0 - i.e. no pillowing - by default.
+// The coeff is really param of the 'sink' vertex effect - if it is not equal to 1.0, we apply the
+// sink effect [centered at (0,0,0)] to the whole mesh as the last step of composing it.
+
+  public float getPillowCoeff()
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public TouchControl getTouchControl()
+    {
+    if( mTouchControl==null )
+      {
+      switch(getTouchControlType())
+        {
+        case TC_TETRAHEDRON      : mTouchControl = new TouchControlTetrahedron(this);
+                                   break;
+        case TC_HEXAHEDRON       : mTouchControl = new TouchControlHexahedron(this);
+                                   break;
+        case TC_OCTAHEDRON       : mTouchControl = new TouchControlOctahedron(this);
+                                   break;
+        case TC_DODECAHEDRON     : mTouchControl = new TouchControlDodecahedron(this);
+                                   break;
+        case TC_ICOSAHEDRON      : mTouchControl = new TouchControlIcosahedron(this);
+                                   break;
+        case TC_CUBOID           : mTouchControl = new TouchControlCuboids(this,getDist3D(getNumLayers()));
+                                   break;
+        case TC_BALL             : mTouchControl = new TouchControlBall(this);
+                                   break;
+        case TC_BARREL           : mTouchControl = new TouchControlBarrel(this,getDist3D(getNumLayers()));
+                                   break;
+        case TC_CHANGING_MIRROR  : mTouchControl = new TouchControlMirror(this);
+                                   break;
+        case TC_CHANGING_SQUARE  : mTouchControl = new TouchControlSquare(this);
+                                   break;
+        case TC_CHANGING_SHAPEMOD: mTouchControl = new TouchControlShapemod(this);
+                                   break;
+        }
+      }
+    return mTouchControl;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[][] returnRotationFactor()
+    {
+    float[][] factor = new float[mNumAxis][];
+
+    for(int ax=0; ax<mNumAxis; ax++)
+      {
+      int numL = mNumLayers[ax];
+      factor[ax] = new float[numL];
+      for(int la=0; la<numL; la++) factor[ax][la] = 1.0f;
+      }
+
+    return factor;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected void setReader(JsonReader reader) { }
+  public String getInventor()                 { return mMetadata.getAuthor(); }
+  public int getYearOfInvention()             { return mMetadata.getYearOfInvention(); }
+  public float getComplexity()                { return mMetadata.getDifficulty(); }
+  public String getObjectName()               { return mMetadata.getObjectName(); }
+  public ObjectSignature getSignature()       { return mMetadata.getSignature(); }
+  public int getCategory()                    { return mMetadata!=null ? mMetadata.getCategory() : 0; }
+  public boolean isSubmittable()              { return mCurrentColorSchemeSubmittable; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  // for JSON only
+  public abstract int getTouchControlType();
+  public abstract int getTouchControlSplit();
+  public abstract boolean[][] getLayerRotatable(int[] numLayers);
+  public abstract int[][][] getEnabled();
+  public abstract float[] getDist3D(int[] numLayers);
+  public abstract Static3D[] getFaceAxis();
+  public abstract int[][] getScrambleEdges();
+  public abstract float[][] getCuts(int[] numLayers);
+  public abstract float getStickerRadius();
+  public abstract float getStickerStroke();
+  public abstract float[][][] getStickerAngles();
+  public abstract int getCubitVariant(int cubit, int[] numLayers);
+  public abstract ObjectShape getObjectShape(int variant);
+  public abstract ObjectFaceShape getObjectFaceShape(int variant);
+  public abstract ObjectVertexEffects getVertexEffects(int variant);
+  public abstract int getNumCubitVariants(int[] numLayers);
+  public abstract float[][] getCubitPositions(int[] numLayers);
+  public abstract Static4D getCubitQuats(int cubit, int[] numLayers);
+  public abstract float getScreenRatio();
+  public abstract int[] getColorTable();
+  public abstract String getShortName();
+  public abstract String printColor(int color);
+
+  // not only for JSON
+  public abstract Static3D[] getRotationAxis();
+  public abstract int[][] getBasicAngles();
+  public abstract int getNumPuzzleFaces();
+  public abstract int getFOV();
+  public abstract String[][] getTutorials();
+
+ */
+  }
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObjectSolved.java b/src/main/java/org/distorted/objectlib/main/TwistyObjectSolved.java
index 73c2758f..248d0bb8 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyObjectSolved.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObjectSolved.java
@@ -119,7 +119,7 @@ class TwistyObjectSolved
 // Color index, i.e. the index into the 'FACE_COLORS' table.
 // else (edge or corner cubit, more than one non-black face): return -2.
 
-  private int retCubitSolvedStatus(int cubit, int[] numLayers, int numCubitFaces, int[][] cubitFaceColors)
+  private int retCubitSolvedStatus(int cubit, int[] numLayers, int numCubitFaces)
     {
     int numNonBlack=0, nonBlackIndex=-1, stiShape, cubColor;
     int variant = mParent.getCubitVariant(cubit,numLayers);
@@ -127,8 +127,7 @@ class TwistyObjectSolved
     for(int face=0; face<numCubitFaces; face++)
       {
       stiShape = mParent.getVariantStickerShape(variant,face);
-      int numFaces = cubitFaceColors[cubit].length;
-      cubColor = face<numFaces ? cubitFaceColors[cubit][face] : -1;
+      cubColor = mParent.getCubitFaceMap(cubit,face);
 
       if( stiShape>=0 && cubColor>=0 )
         {
@@ -600,7 +599,7 @@ class TwistyObjectSolved
 // Called from TwistyObject
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  int[][] getSolvedQuats(int[] numLayers, int numCubitFaces, int[][] cubitFaceColors)
+  int[][] getSolvedQuats(int[] numLayers, int numCubitFaces)
     {
     int[] groups = new int[mNumCubits];
     int numGroups = 1;
@@ -608,7 +607,7 @@ class TwistyObjectSolved
 
     for(int cubit=0; cubit<mNumCubits; cubit++)
       {
-      groups[cubit] = retCubitSolvedStatus(cubit,numLayers,numCubitFaces,cubitFaceColors);
+      groups[cubit] = retCubitSolvedStatus(cubit,numLayers,numCubitFaces);
       if( groups[cubit]>=0 ) numGroups++;
       else                   numFirst++;
       }
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java b/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java
index 64761565..7ecc56c8 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java
@@ -1,5 +1,5 @@
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
+// Copyright 2024 Leszek Koltunski                                                               //
 //                                                                                               //
 // This file is part of Magic Cube.                                                              //
 //                                                                                               //
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObjectWithMesh.java b/src/main/java/org/distorted/objectlib/main/TwistyObjectWithMesh.java
new file mode 100644
index 00000000..fa3a0cf6
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObjectWithMesh.java
@@ -0,0 +1,490 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2024 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlib.main;
+
+import org.distorted.library.effect.MatrixEffectMove;
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectSink;
+import org.distorted.library.helpers.QuatHelper;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshFile;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.objectlib.helpers.FactoryCubit;
+import org.distorted.objectlib.helpers.ObjectFaceShape;
+import org.distorted.objectlib.helpers.ObjectShape;
+import org.distorted.objectlib.helpers.ObjectVertexEffects;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+import org.distorted.objectlib.metadata.Metadata;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class TwistyObjectWithMesh extends TwistyObjectTheoretical
+  {
+  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);
+
+  protected static final Static3D CENTER = new Static3D(0,0,0);
+
+  protected int[] mNumLayers;
+  protected MeshBase mMesh;
+  protected DistortedEffects mEffects;
+  protected int mNumCubitFaces;
+
+  private Static4D[] mCubitOrigQuat;
+  private MeshBase[] mMeshes;
+  private Static4D[] mMixupModeQuats;
+  private boolean mIsInMixupMode;
+  private ObjectShape[] mShapes;
+  private int mNumCubitVariants;
+  private int[][] mCubitFaceColors;
+  private int[][] mVariantFaceIsOuter;
+  private int mMaxNumLayers;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void initialize(InitAssets asset, boolean fromJSON)
+    {
+    Static3D[] axis = getRotationAxis();
+    int[][] angles = getBasicAngles();
+    Static3D[] faceAxis = getFaceAxis();
+    float[][] positions = getCubitPositions(mNumLayers);
+    float[][] cuts = getCuts(mNumLayers);
+    int[][] minCubits = getMinimalCubiesInRow();
+
+    int numCubits = positions.length;
+    float[][] offsets = new float[numCubits][];
+    int[] types = new int[numCubits];
+
+    for(int c=0; c<numCubits; c++)
+      {
+      offsets[c] = getCubitRowOffset(c);
+      types[c] = getCubitRotationType(c);
+      }
+
+    super.initialize(axis, angles, positions, cuts, minCubits, offsets, types, faceAxis);
+
+    mCubitOrigQuat = new Static4D[mNumCubits];
+    for(int i=0; i<mNumCubits; i++) mCubitOrigQuat[i] = getCubitQuats(i,mNumLayers);
+
+    mMaxNumLayers = -1;
+    for(int i=0; i<mNumAxis; i++)
+      {
+      if( mMaxNumLayers<mNumLayers[i] ) mMaxNumLayers = mNumLayers[i];
+      }
+
+    mEffects = new DistortedEffects();
+    createQuaternionEffects();
+
+    InputStream meshStream = asset!=null ? asset.getMeshStream(): null;
+    boolean fromDMESH = (meshStream!=null);
+    getShapes(fromDMESH,fromJSON);
+    createWholeMesh(meshStream,fromDMESH);
+
+    for(int c=0; c<mNumCubits; c++) setCubitQuat(c,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createQuaternionEffects()
+    {
+    if( mNumQuats<=ObjectControl.MAX_QUATS )
+      {
+      mIsInMixupMode = false;
+
+      for( int q=0; q<mNumQuats; q++)
+        {
+        VertexEffectQuaternion vq = new VertexEffectQuaternion(mObjectQuats[q],CENTER);
+        vq.setMeshAssociation(0,q);
+        mEffects.apply(vq);
+        }
+      }
+    else if( mNumCubits<=ObjectControl.MAX_QUATS )
+      {
+      mIsInMixupMode = true;
+      mMixupModeQuats = new Static4D[mNumCubits];
+
+      for( int q=0; q<mNumCubits; q++)
+        {
+        mMixupModeQuats[q] = new Static4D(mObjectQuats[0]);
+        VertexEffectQuaternion vq = new VertexEffectQuaternion(mMixupModeQuats[q],CENTER);
+        vq.setMeshAssociation(0,q);
+        mEffects.apply(vq);
+        }
+      }
+    else
+      {
+      android.util.Log.e("D", "object has too many quaternions ("+mNumQuats+") or too many cubits ("+mNumCubits+")");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 createOuterFaces()
+    {
+    for(int v=0; v<mNumCubitVariants; v++)
+      {
+      int numFaces = mShapes[v].getNumFaces();
+      mVariantFaceIsOuter[v] = new int[numFaces];
+      }
+
+    for( int cubit=0; cubit<mNumCubits; cubit++)
+      {
+      int variant = getCubitVariant(cubit,mNumLayers);
+      int numFaces = mShapes[variant].getNumFaces();
+
+      for(int face=0; face<numFaces; face++)
+        if( getCubitFaceMap(cubit,face)>=0 ) mVariantFaceIsOuter[variant][face] = 1;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void getShapes(boolean fromDMESH, boolean fromJSON)
+    {
+    mNumCubitVariants = getNumCubitVariants(mNumLayers);
+
+    if( fromJSON ) mNumCubitFaces = getNumCubitFaces();
+
+    if( !fromDMESH || !fromJSON )
+      {
+      FactoryCubit factory = FactoryCubit.getInstance();
+      factory.clear();
+
+      mShapes = new ObjectShape[mNumCubitVariants];
+      for(int v=0; v<mNumCubitVariants; v++) mShapes[v] = getObjectShape(v);
+      mNumCubitFaces = ObjectShape.computeNumComponents(mShapes);
+      mVariantFaceIsOuter = new int[mNumCubitVariants][];
+
+      if( !fromJSON )
+        {
+        mCubitFaceColors = ObjectShape.computeColors(mShapes,mCubitPos, mCubitOrigQuat,this);
+        createOuterFaces();
+        }
+
+      if( fromDMESH )
+        {
+        for(int i=0; i<mNumCubitVariants; i++) factory.createNewFaceTransform(mShapes[i], mVariantFaceIsOuter[i]);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createWholeMesh(InputStream stream, boolean fromDMESH)
+    {
+    if( fromDMESH )
+      {
+      DataInputStream dos = new DataInputStream(stream);
+      mMesh = new MeshFile(dos);
+
+      try
+        {
+        stream.close();
+        }
+      catch(IOException e)
+        {
+        android.util.Log.e("meshFile", "Error closing InputStream: "+e);
+        }
+      }
+    else
+      {
+      MeshBase[] cubitMesh = new MeshBase[mNumCubits];
+
+      for(int i=0; i<mNumCubits; i++)
+        {
+        cubitMesh[i] = createSingleCubitMesh(i,mNumLayers,mNumCubitFaces);
+        Static3D pos = getPos(mCubitPos[i]);
+        cubitMesh[i].apply(new MatrixEffectMove(pos),1,0);
+        }
+
+      mMesh = new MeshJoined(cubitMesh);
+
+      float pillowCoeff = getPillowCoeff();
+
+      if( pillowCoeff!=1.0f )
+        {
+        float radius = getCircumscribedRadius();
+        Static1D coeff = new Static1D(pillowCoeff);
+        Static4D region= new Static4D(0,0,0,radius);
+        VertexEffectSink sink = new VertexEffectSink(coeff,CENTER,region);
+        mMesh.apply(sink);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private MeshBase createSingleCubitMesh(int cubit, int[] numLayers, int numComponents)
+    {
+    int variant = getCubitVariant(cubit,numLayers);
+
+    if( mMeshes==null ) mMeshes = new MeshBase[mNumCubitVariants];
+
+    if( mMeshes[variant]==null )
+      {
+      ObjectFaceShape faceShape = getObjectFaceShape(variant);
+      ObjectVertexEffects effects = getVertexEffects(variant);
+      FactoryCubit factory = FactoryCubit.getInstance();
+      factory.createNewFaceTransform(mShapes[variant],mVariantFaceIsOuter[variant]);
+      mMeshes[variant] = factory.createRoundedSolid(mShapes[variant],faceShape,effects,numComponents);
+      }
+
+    MeshBase mesh = mMeshes[variant].copy(true);
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( mCubitOrigQuat[cubit], CENTER );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void rotateAllCubits(int axisIndex, int rowBitmap, int angle)
+    {
+    super.rotateAllCubits(axisIndex,rowBitmap,angle);
+    for(int i=0; i<mNumCubits; i++) setCubitQuat(i,getCubitQuatIndex(i));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitFaceMap(int cubit, int face)
+    {
+    int numFaces = mCubitFaceColors[cubit].length;
+    int puzzleFace = face<numFaces ? mCubitFaceColors[cubit][face] : -1;
+    return puzzleFace<0 ? -1 : puzzleFace;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this would return the REAL value of outer; the 'getCubitFaceColor()>=0' returns if the face is
+// colored, which for example in case of Container (which has colored internal faces) is not the
+// same thing
+
+  boolean faceIsOuter(int cubit, int face)
+    {
+    if( mCubitFaceColors==null )
+      {
+      if( mShapes   ==null ) mShapes = new ObjectShape[mNumCubitVariants];
+      if( mShapes[0]==null ) for(int v=0; v<mNumCubitVariants; v++) mShapes[v] = getObjectShape(v);
+
+      if( mCubitOrigQuat   ==null ) mCubitOrigQuat = new Static4D[mNumCubits];
+      if( mCubitOrigQuat[0]==null ) for(int c=0; c<mNumCubits; c++) mCubitOrigQuat[c] = getCubitQuats(c,mNumLayers);
+
+      mCubitFaceColors = ObjectShape.computeColors(mShapes,mCubitPos, mCubitOrigQuat,this);
+      }
+
+    return mCubitFaceColors[cubit][face]>=0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void releaseResources()
+    {
+    super.releaseResources();
+    mMesh.markForDeletion();
+    mEffects.markForDeletion();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setCubitQuat(int cubit, int equAssociation)
+    {
+    int andAssociation=0, accumulativeShift=0;
+
+    for(int axis=0; axis<mNumAxis; axis++)
+      {
+      int rotRow = getCubitRotRow(cubit,axis);
+      andAssociation += (rotRow<<accumulativeShift);
+      accumulativeShift += mMaxNumLayers;
+      }
+
+    if( !mIsInMixupMode )
+      {
+      mMesh.setEffectAssociation(cubit,andAssociation,equAssociation);
+      }
+    else
+      {
+      mMesh.setEffectAssociation(cubit,andAssociation,cubit);
+      Static4D tmp = mObjectQuats[equAssociation];
+      mMixupModeQuats[cubit].set(tmp);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Return the 'surface' the given face of the given cubit is on. (px,py,pz) is the pre-computed
+// center of the cubit (so we don't have to keep re-computing this per each face)
+// If the face is not external, return null.
+//
+// This is needed by the TwistyObjectSolved's method2 detecting if the object is in a solved state.
+//
+// What is a 'surface' ? For objects with flat monochromatic external walls, it is simply a 4-tuple
+// [(nx,ny,nz),d] (normal vector + distance from origin) describing a 3D plane this face is part of.
+// Exceptions are objects with curved external walls (ATM Masterball & Penrose Cubes): it is a union
+// of several surfaces which together define the bent, monochromatic wall. I.e. Penrose Cube has 3
+// surfaces (blue, yellow and red), each defined by 3 planes (think before the bending effect!)
+//
+// This is the default which simply returns the single plane. If some puzzle has more complicated
+// surfaces, it needs to override this method.
+//
+// We never need to test if a given point is part of a given surface (see TwistyObjectSurface!) - we
+// only need to be able to rotate the surface by quats and test if two surfaces are the same. Thus:
+// it is possible to provide some artificial surfaces - for example, in case of the Penrose, the
+// three surfaces can be only the single 'middle' plane [the one on the bend].
+
+  public float[] getMonochromaticSurface(int variant, int cubitIndex, int faceIndex, float px, float py, float pz)
+    {
+    if( !faceIsOuter(cubitIndex,faceIndex) ) return null;
+
+    Static4D cubitQuat = getCubitQuats(cubitIndex,mNumLayers);
+
+    float[] normal   = new float[4];
+    float[] intPoint = new float[3];
+    float[] rotPoint = new float[4];
+    float[] surface  = new float[4];
+
+    ObjectShape objShape = mShapes[variant];
+    objShape.getFacePoint(faceIndex,intPoint);
+    objShape.getFaceNormal(faceIndex,normal);
+
+    QuatHelper.rotateVectorByQuat(rotPoint,intPoint[0],intPoint[1],intPoint[2],1,cubitQuat);
+    QuatHelper.rotateVectorByQuat(surface,normal[0],normal[1],normal[2],0,cubitQuat);
+
+    float x = rotPoint[0] + px;
+    float y = rotPoint[1] + py;
+    float z = rotPoint[2] + pz;
+    surface[3] = x*surface[0] + y*surface[1] + z*surface[2];
+
+    return surface;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean restorePreferences(String key, OperatingSystemInterface os)
+    {
+    boolean error = super.restorePreferences(key,os);
+
+    if( error )
+      {
+      for(int c=0; c<mNumCubits; c++) setCubitQuat(c,0);
+      }
+    else
+      {
+      for(int c=0; c<mNumCubits; c++)
+        {
+        int q = getCubitQuatIndex(c);
+        setCubitQuat(c, q);
+        }
+      }
+
+    return error;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Radius of the sphere circumscribed on the puzzle. Needed for pillowing.
+//
+// This won't work correctly for pillowing off-center puzzles (e.g. mirrors) - for those we'd need
+// to introduce the concept of a 'sink center' as well.
+//
+// public because needed in TouchControlShapemod
+
+  public float getCircumscribedRadius()
+    {
+    float size = getSize();
+
+    switch( getCategory() )
+      {
+      case Metadata.CATEGORY_SHAPE_TET: return (SQ6/4)*size;
+      case Metadata.CATEGORY_SHAPE_HEX: return (SQ3/2)*size;
+      case Metadata.CATEGORY_SHAPE_OCT: return (SQ2/2)*size;
+      case Metadata.CATEGORY_SHAPE_DOD: return (SQ3/2)*((SQ5+1)/2)*size;
+      case Metadata.CATEGORY_SHAPE_BAL: return 0.50f*size;
+      case Metadata.CATEGORY_SHAPE_ICO: return 0; // no pillowed icosahedra
+      case Metadata.CATEGORY_SHAPE_BAR: return 0; // no pillowed barrels
+      }
+
+    return 0.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// INTERNAL API - those are called from 'effects' package
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public MeshBase getObjectMesh()            { return mMesh; }
+  public DistortedEffects getObjectEffects() { return mEffects; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void solve()
+    {
+    super.solve();
+    for(int i=0; i<mNumCubits; i++) setCubitQuat(i,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float getPillowCoeff()                     { return 1.0f; }
+  public int[][] getMinimalCubiesInRow()            { return null; }
+  public int[] getNumLayers()                       { return mNumLayers; }
+  public int getCubitRotationType(int cubit)        { return TwistyObjectTheoreticalCubit.TYPE_NORMAL; }
+  public float[] getCubitRowOffset(int cubitIndex)  { return null; }
+  public int[][] getVariantFaceIsOuter()            { return mVariantFaceIsOuter; }
+  public int getNumCubitFaces()                     { return 0; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public abstract Static3D[] getFaceAxis();
+  public abstract float[][] getCuts(int[] numLayers);
+  public abstract int getCubitVariant(int cubit, int[] numLayers);
+  public abstract ObjectShape getObjectShape(int variant);
+  public abstract ObjectFaceShape getObjectFaceShape(int variant);
+  public abstract ObjectVertexEffects getVertexEffects(int variant);
+  public abstract int getNumCubitVariants(int[] numLayers);
+  public abstract float[][] getCubitPositions(int[] numLayers);
+  public abstract Static4D getCubitQuats(int cubit, int[] numLayers);
+  public abstract Static3D[] getRotationAxis();
+  public abstract int[][] getBasicAngles();
+  public abstract int getCategory();
+  public abstract float[] getDist3D(int[] numLayers);
+  public abstract float getSize();
+  }
