commit 69661af7eb32b194d5428f2cbead18cd5c57130e
Author: leszek <leszek@koltunski.pl>
Date:   Thu Dec 5 15:26:43 2024 +0100

    First stage of the planned split of the giant TwistyObject class into (at first!) two: TwistyObjectTheoretical (a 'theoretical' object which moves just like the real one, but which has no mesh, stickers, shapes, node and cannot be controlled) and everything else.

diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObjectCubitTheoretical.java b/src/main/java/org/distorted/objectlib/main/TwistyObjectCubitTheoretical.java
new file mode 100644
index 00000000..9e41c653
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObjectCubitTheoretical.java
@@ -0,0 +1,301 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.helpers.QuatHelper;
+import org.distorted.library.type.Static4D;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TwistyObjectCubitTheoretical
+  {
+  public static final int TYPE_NORMAL   = 0;  // all 'old', 'pre-crazy' cubits
+  public static final int TYPE_FOLLOWER = 1;  // those cubits that follow deciders (e.g. small circle edges from Crazy Plus planets)
+  public static final int TYPE_DECIDER  = 2;  // cubits deciding about rotation rows of their followers (e.g. center cubits of Crazy Plus Planets)
+
+  private final float[] mTmp;
+  private final float[] mOrigPosition;
+  private final float[] mCurrentPosition;
+  private final float[] mRowOffset;
+  private final float mOrigOffsetX, mOrigOffsetY, mOrigOffsetZ;
+  private final int mNumAxis;
+  private final int mLen;
+  private final int[] mRotationRow;
+  private final int mCubitType;
+  private final int mOrigPuzzleFace;
+  private final int mCubitIndex;
+
+  private float[] mTrackingPoint;
+  private int mCurrentPuzzleFace;
+  private TwistyObjectTheoretical mParent;
+
+  int mQuatIndex;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyObjectCubitTheoretical(TwistyObjectTheoretical parent, float[] position, int numAxis, int cubitIndex)
+    {
+    mQuatIndex= 0;
+    mParent   = parent;
+    mLen      = position.length;
+
+    mCubitType    = mParent.getCubitRotationType(cubitIndex);
+    mRowOffset    = mParent.getCubitRowOffset(cubitIndex);
+    mTrackingPoint= mParent.getTrackingPoint(cubitIndex,mCubitType);
+    int face      = mParent.computeCurrentPuzzleFace(mCubitType,mTrackingPoint);
+
+    if( mRowOffset!=null )
+      {
+      mOrigOffsetX = mRowOffset[0];
+      mOrigOffsetY = mRowOffset[1];
+      mOrigOffsetZ = mRowOffset[2];
+      if( mCubitType==TYPE_DECIDER ) mParent.setRotationRowOffset(face,mRowOffset);
+      }
+    else
+      {
+      mOrigOffsetX = 0;
+      mOrigOffsetY = 0;
+      mOrigOffsetZ = 0;
+      }
+
+    mCubitIndex       = cubitIndex;
+    mOrigPuzzleFace   = face;
+    mCurrentPuzzleFace= face;
+    mOrigPosition     = new float[mLen];
+    mCurrentPosition  = new float[mLen];
+    mTmp              = new float[4];
+
+    for(int i=0; i<mLen; i++)
+      {
+      mOrigPosition[i]    = position[i];
+      mCurrentPosition[i] = position[i];
+      }
+
+    mNumAxis     = numAxis;
+    mRotationRow = new int[mNumAxis];
+    computeRotationRow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Because of quatMultiplication, errors can accumulate - so to avoid this, we
+// correct the value of the 'scramble' quat to what it should be - one of the legal quats from the
+// list QUATS.
+//
+// We also have to remember that the group of unit quaternions is a double-cover of rotations
+// in 3D ( q represents the same rotation as -q ) - so invert if needed.
+
+  private int normalizeScrambleQuat(Static4D quat)
+    {
+    float x = quat.get0();
+    float y = quat.get1();
+    float z = quat.get2();
+    float w = quat.get3();
+    float xd,yd,zd,wd;
+    float diff, mindiff = Float.MAX_VALUE;
+    int ret=0;
+    int num_quats = mParent.mObjectQuats.length;
+    Static4D qt;
+
+    for(int q=0; q<num_quats; q++)
+      {
+      qt = mParent.mObjectQuats[q];
+
+      xd = x - qt.get0();
+      yd = y - qt.get1();
+      zd = z - qt.get2();
+      wd = w - qt.get3();
+
+      diff = xd*xd + yd*yd + zd*zd + wd*wd;
+
+      if( diff < mindiff )
+        {
+        ret = q;
+        mindiff = diff;
+        }
+
+      xd = x + qt.get0();
+      yd = y + qt.get1();
+      zd = z + qt.get2();
+      wd = w + qt.get3();
+
+      diff = xd*xd + yd*yd + zd*zd + wd*wd;
+
+      if( diff < mindiff )
+        {
+        ret = q;
+        mindiff = diff;
+        }
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void computeRotationRow()
+    {
+    for(int i=0; i<mNumAxis; i++)
+      {
+      mRotationRow[i] = mParent.computeRow(mCurrentPosition,i,mCubitType,mCurrentPuzzleFace);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String createKey(String key)
+    {
+    return key+"_"+mCubitIndex+"_"+mOrigPosition[0]+"_"+mOrigPosition[1]+"_"+mOrigPosition[2];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void savePreferences(String key, OperatingSystemInterface os)
+    {
+    os.putInt(createKey(key), mQuatIndex);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void removePreferences(String key, OperatingSystemInterface os)
+    {
+    os.remove(createKey(key));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int restorePreferences(String key, OperatingSystemInterface os)
+    {
+    mQuatIndex = os.getInt(createKey(key), 0);
+    return mQuatIndex;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getCurrentPos()
+    {
+    return mCurrentPosition;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void rotateCubit(Static4D quat, boolean clamp)
+    {
+    int len = mLen/3;
+
+    if( clamp )
+      {
+      for(int i=0; i<len; i++)
+        {
+        QuatHelper.rotateVectorByQuat( mTmp, mCurrentPosition[3*i], mCurrentPosition[3*i+1], mCurrentPosition[3*i+2], 0, quat);
+        mCurrentPosition[3*i  ] = mTmp[0];
+        mCurrentPosition[3*i+1] = mTmp[1];
+        mCurrentPosition[3*i+2] = mTmp[2];
+        mParent.clampPos(mCurrentPosition, 3*i);
+        }
+      }
+    else
+      {
+      for(int i=0; i<len; i++)
+        {
+        QuatHelper.rotateVectorByQuat( mTmp, mCurrentPosition[3*i], mCurrentPosition[3*i+1], mCurrentPosition[3*i+2], 0, quat);
+        mCurrentPosition[3*i  ] = mTmp[0];
+        mCurrentPosition[3*i+1] = mTmp[1];
+        mCurrentPosition[3*i+2] = mTmp[2];
+        }
+      }
+
+    if( mCubitType!=TYPE_NORMAL )
+      {
+      QuatHelper.rotateVectorByQuat( mTmp, mTrackingPoint[0], mTrackingPoint[1], mTrackingPoint[2], 0, quat);
+      mTrackingPoint[0] = mTmp[0];
+      mTrackingPoint[1] = mTmp[1];
+      mTrackingPoint[2] = mTmp[2];
+      mCurrentPuzzleFace = mParent.computeCurrentPuzzleFace(mCubitType,mTrackingPoint);
+
+      if( mCurrentPuzzleFace<0 )  // unexpected error
+        {
+        mCurrentPuzzleFace = 0;
+        }
+      }
+
+    if( mCubitType==TYPE_DECIDER )
+      {
+      QuatHelper.rotateVectorByQuat( mTmp, mRowOffset[0], mRowOffset[1], mRowOffset[2], 0, quat);
+      mRowOffset[0] = mTmp[0];
+      mRowOffset[1] = mTmp[1];
+      mRowOffset[2] = mTmp[2];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void postRotateCubit(Static4D quat)
+    {
+    Static4D q = QuatHelper.quatMultiply(quat,mParent.mObjectQuats[mQuatIndex]);
+    mQuatIndex = normalizeScrambleQuat(q);
+    computeRotationRow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void solve()
+    {
+    mQuatIndex = 0;
+    mCurrentPuzzleFace = mOrigPuzzleFace;
+    System.arraycopy(mOrigPosition, 0, mCurrentPosition, 0, mCurrentPosition.length);
+
+    if( mCubitType!=TYPE_NORMAL )
+      {
+      mTrackingPoint = mParent.getTrackingPoint(mCubitIndex,mCubitType);
+      if( mCubitType==TYPE_DECIDER )
+        {
+        mRowOffset[0] = mOrigOffsetX;
+        mRowOffset[1] = mOrigOffsetY;
+        mRowOffset[2] = mOrigOffsetZ;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void releaseResources()
+    {
+    mParent = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getType()
+    {
+    return mCubitType;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float[] getOffset()
+    {
+    return mRowOffset;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getPuzzleFace()
+    {
+    return mCurrentPuzzleFace;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getRotRow(int axisIndex)
+    {
+    return mRotationRow[axisIndex];
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java b/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java
new file mode 100644
index 00000000..0986eddb
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObjectTheoretical.java
@@ -0,0 +1,633 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+import org.distorted.objectlib.helpers.QuatGroupGenerator;
+import org.distorted.objectlib.metadata.Metadata;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class TwistyObjectTheoretical
+  {
+  protected Static4D[] mObjectQuats;  // example: 3x3 Cube. This is the quat group (order 24).
+  private TwistyObjectCubitTheoretical[] mCubits;
+  private int mNumCubits, mNumQuats;
+  private Static3D[] mAxis;
+  private float[][] mCuts;
+  private int[][] mMinimalCubiesInRow;
+  private int[] mNumCuts;
+  private float[][] mOrigPos;
+  private boolean mIsBandaged;
+  private int[][] mBasicAngles;
+  private float[][] mRowOffsets;
+  private boolean[] mBelongs;
+  private int mNumAxis;
+  private boolean mThereAreDeciders;
+  private final int[] mNumLayers;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public TwistyObjectTheoretical(Metadata meta)
+    {
+    mNumLayers = meta.getNumLayers();
+    initialize();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public abstract boolean[][] getLayerRotatable(int[] numLayers);
+  public abstract float[][] getCuts(int[] numLayers);
+  public abstract float[][] getCubitPositions(int[] numLayers);
+  public abstract Static3D[] getRotationAxis();
+  public abstract int[][] getBasicAngles();
+
+  public abstract int getNumPuzzleFaces();
+  public abstract int getCubitRotationType(int cubit);
+  public abstract float[] getCubitRowOffset(int cubit);
+  public abstract int[][] getMinimalCubiesInRow();
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void initialize()
+    {
+    mAxis = getRotationAxis();
+    mBasicAngles = getBasicAngles();
+    mObjectQuats = getQuats();
+    mNumQuats = mObjectQuats.length;
+    mOrigPos = getCubitPositions(mNumLayers);
+    mRowOffsets = new float[getNumPuzzleFaces()][3];
+    mNumAxis = mAxis.length;
+    mCuts = getCuts(mNumLayers);
+    mNumCuts = new int[mNumAxis];
+
+    for(int i=0; i<mNumAxis; i++)
+      {
+      mNumCuts[i] = (mCuts==null || mCuts[i]==null ? 0 : mCuts[i].length);
+      }
+
+    mMinimalCubiesInRow = getMinimalCubiesInRow();
+    mNumCubits = mOrigPos.length;
+    mBelongs = new boolean[mNumCubits];
+
+    boolean bandaged=false;
+
+    for( int c=0; c<mNumCubits; c++)
+      {
+      if( mOrigPos[c].length>3 )
+        {
+        bandaged=true;
+        break;
+        }
+      }
+    mIsBandaged = bandaged;
+
+    mCubits = new TwistyObjectCubitTheoretical[mNumCubits];
+
+    for(int i=0; i<mNumCubits; i++)
+      {
+      mCubits[i] = new TwistyObjectCubitTheoretical(this,mOrigPos[i],mNumAxis,i);
+      if( !mThereAreDeciders && mCubits[i].getType()==TwistyObjectCubit.TYPE_DECIDER ) mThereAreDeciders = 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;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getTrackingPoint(int cubitIndex, int cubitType)
+    {
+    /*
+    if( cubitType!=TwistyObjectCubit.TYPE_NORMAL )
+      {
+      int variant = getCubitVariant(cubitIndex,mNumLayers);
+
+      // object must have been created from JSON
+      if( mVariantFaceIsOuter==null || mVariantFaceIsOuter[variant]==null )
+        {
+        mVariantFaceIsOuter = getVariantFaceIsOuter();
+        }
+      if( mShapes==null )
+        {
+        mShapes = new ObjectShape[mNumCubitVariants];
+        }
+      if( mShapes[variant]==null )
+        {
+        mShapes[variant] = getObjectShape(variant);
+        }
+      if( mOrigQuat==null )
+        {
+        mOrigQuat = new Static4D[mNumCubits];
+        }
+      if( mOrigQuat[cubitIndex]==null )
+        {
+        mOrigQuat[cubitIndex] = getCubitQuats(cubitIndex,mNumLayers);
+        }
+
+      int outer=-1, faces = mShapes[variant].getNumFaces();
+
+      for(int i=0; i<faces; i++)
+        {
+        if( mVariantFaceIsOuter[variant][i]==1 )
+          {
+          outer=i;
+          break;
+          }
+        }
+
+      if( outer>=0 )
+        {
+        float[] v = mShapes[variant].getFirstVertexInFace(outer);
+        float[] ret = new float[3];
+        float[] curpos = mOrigPos[cubitIndex];
+        Static4D quat = mOrigQuat[cubitIndex];
+        QuatHelper.rotateVectorByQuat(mTmp, v[0], v[1], v[2], 1, quat);
+
+        ret[0] = mTmp[0]+computeAvg(curpos,0);
+        ret[1] = mTmp[1]+computeAvg(curpos,1);
+        ret[2] = mTmp[2]+computeAvg(curpos,2);
+
+        return ret;
+        }
+      else
+        {
+        android.util.Log.e("D", "Error in getTrackingPoint: no outer face??");
+        }
+      }
+    */
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeCurrentPuzzleFace(int type, float[] vertex)
+    {
+    /*
+    if( type!=TwistyObjectCubit.TYPE_NORMAL )
+      {
+      Static3D[] axis = getFaceAxis();
+      float[] dist3D = getDist3D(mNumLayers);
+      final float MAXERR = 0.98f;
+      int numAxis = axis.length;
+      float x = vertex[0];
+      float y = vertex[1];
+      float z = vertex[2];
+
+      for(int i=0; i<numAxis; i++)
+        {
+        Static3D ax = axis[i];
+        float len = ax.get0()*x + ax.get1()*y + ax.get2()*z;
+        if( len>mSize*dist3D[i]*MAXERR ) return i;
+        }
+
+      return -2;
+      }
+    */
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setRotationRowOffset(int puzzleFace, float[] offset)
+    {
+    mRowOffsets[puzzleFace][0] = offset[0];
+    mRowOffsets[puzzleFace][1] = offset[1];
+    mRowOffsets[puzzleFace][2] = offset[2];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumAxis()
+    {
+    return mNumAxis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int computeRow(float[] pos, int axisIndex, int cubitType, int puzzleFace)
+    {
+    int ret=0;
+    int len = pos.length / 3;
+    Static3D axis = mAxis[axisIndex];
+    float axisX = axis.get0();
+    float axisY = axis.get1();
+    float axisZ = axis.get2();
+    float casted, xoff=0, yoff=0, zoff=0;
+
+    if( cubitType!=TwistyObjectCubit.TYPE_NORMAL )
+      {
+      xoff = mRowOffsets[puzzleFace][0];
+      yoff = mRowOffsets[puzzleFace][1];
+      zoff = mRowOffsets[puzzleFace][2];
+      }
+
+    for(int i=0; i<len; i++)
+      {
+      casted = (pos[3*i]+xoff)*axisX + (pos[3*i+1]+yoff)*axisY + (pos[3*i+2]+zoff)*axisZ;
+      ret |= computeSingleRow(axisIndex,casted);
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeSingleRow(int axisIndex,float casted)
+    {
+    int num = mNumCuts[axisIndex];
+    float[] cuts = mCuts[axisIndex];
+
+    for(int i=0; i<num; i++)
+      {
+      if( casted<cuts[i] ) return (1<<i);
+      }
+
+    return (1<<num);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean belongsToRotation( int cubit, int axis, int rowBitmap)
+    {
+    return (mCubits[cubit].getRotRow(axis) & rowBitmap) != 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// note the minus in front of the sin() - we rotate counterclockwise
+// when looking towards the direction where the axis increases in values.
+
+  private Static4D makeQuaternion(float axisX, float axisY, float axisZ, int angleInDegrees)
+    {
+    while( angleInDegrees<0 ) angleInDegrees += 360;
+    angleInDegrees %= 360;
+    
+    float cosA = (float)Math.cos(Math.PI*angleInDegrees/360);
+    float sinA =-(float)Math.sqrt(1-cosA*cosA);
+
+    return new Static4D(axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void rotateAllCubits(int axisIndex, int rowBitmap, int angle)
+    {
+    Static3D axis = mAxis[axisIndex];
+    float axisX = axis.get0();
+    float axisY = axis.get1();
+    float axisZ = axis.get2();
+    Static4D quat = makeQuaternion(axisX,axisY,axisZ,angle);
+
+    for(int i=0; i<mNumCubits; i++)
+      {
+      mBelongs[i] = belongsToRotation(i,axisIndex,rowBitmap);
+      if( mBelongs[i] ) mCubits[i].rotateCubit(quat,false);
+      }
+
+    recomputeFaceOffsets();
+
+    for(int i=0; i<mNumCubits; i++)
+      {
+      if( mBelongs[i] )
+        {
+        mCubits[i].postRotateCubit(quat);
+        }
+      else if( mCubits[i].getType()==TwistyObjectCubitTheoretical.TYPE_FOLLOWER )
+        {
+        mCubits[i].computeRotationRow();
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private synchronized void setupPosition(int[][] moves)
+    {
+    if( moves!=null )
+      {
+      int axisIndex, row, rowBitmap, basic, angle;
+
+      for(int[] move: moves)
+        {
+        axisIndex= move[0];
+        rowBitmap= computeBandagedBitmap( move[1],axisIndex);
+        row      = computeRowFromBitmap( move[1] );
+        basic    = mBasicAngles[axisIndex][row];
+        angle    = move[2]*(360/basic);   // this assumes that all layers from
+                                          // the bitmap have the same BasicAngle.
+                                          // at the moment this is always true as
+                                          // there are no bandaged objects with
+                                          // different per-layer BasicAngles.
+
+        rotateAllCubits(axisIndex,rowBitmap,angle);
+        }
+
+      for(int i=0; i<mNumCubits; i++)
+        {
+        float[] pos = mCubits[i].getCurrentPos();
+        int len = pos.length/3;
+        for(int j=0; j<len; j++) clampPos(pos,3*j);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int computeBandagedBitmap(int rowBitmap, int axisIndex)
+    {
+    if( mIsBandaged )
+      {
+      int bitmap, initBitmap=0;
+
+      while( initBitmap!=rowBitmap )
+        {
+        initBitmap = rowBitmap;
+
+        for(int cubit=0; cubit<mNumCubits; cubit++)
+          {
+          bitmap = mCubits[cubit].getRotRow(axisIndex);
+          if( (rowBitmap & bitmap) != 0 ) rowBitmap |= bitmap;
+          }
+        }
+      }
+
+    if( mMinimalCubiesInRow!=null )
+      {
+      int[] minC = mMinimalCubiesInRow[axisIndex];
+      int numL = minC.length;
+      int[] numC = new int[numL];
+
+      for(int cubit=0; cubit<mNumCubits; cubit++)
+        {
+        int bitmap = mCubits[cubit].getRotRow(axisIndex);
+
+        for(int i=0; i<numL; i++)
+          {
+          if( (bitmap&0x1)!=0 ) numC[i]++;
+          bitmap>>=1;
+          }
+        }
+
+      int bitmap,initBitmap = 0;
+
+      while( initBitmap!=rowBitmap )
+        {
+        initBitmap = rowBitmap;
+        bitmap = rowBitmap;
+        int last = 0;
+
+        for(int i=0; i<numL; i++)
+          {
+          if( numC[i]<minC[i] && numC[i]>0 )
+            {
+            if( (bitmap&0x1)!=0 )
+              {
+              if( i>0     ) rowBitmap |= (1<<(i-1));
+              if( i<numL-1) rowBitmap |= (1<<(i+1));
+              }
+            else if( (bitmap&0x2)!=0 || last>0 ) // we are not rotating this row, but
+                                                 // we are rotating the next or the prev
+              {
+              rowBitmap |= (1<<i);
+              }
+            }
+
+          last = (bitmap&0x1);
+          bitmap>>=1;
+          }
+        }
+      }
+
+    return rowBitmap;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int computeRowFromBitmap(int rowBitmap)
+    {
+    int index = 0;
+
+    while(index<32)
+      {
+      if( (rowBitmap&0x1) != 0 ) return index;
+      rowBitmap>>=1;
+      index++;
+      }
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Clamp all rotated positions to one of those original ones to avoid accumulating errors.
+// Do so only if minimal Error is appropriately low (shape-shifting puzzles - Square-1)
+
+  void clampPos(float[] pos, int offset)
+    {
+    float currError, minError = Float.MAX_VALUE;
+    int minErrorIndex1 = -1;
+    int minErrorIndex2 = -1;
+
+    float x = pos[offset  ];
+    float y = pos[offset+1];
+    float z = pos[offset+2];
+
+    float xo,yo,zo;
+
+    for(int i=0; i<mNumCubits; i++)
+      {
+      int len = mOrigPos[i].length / 3;
+
+      for(int j=0; j<len; j++)
+        {
+        xo = mOrigPos[i][3*j  ];
+        yo = mOrigPos[i][3*j+1];
+        zo = mOrigPos[i][3*j+2];
+
+        currError = (xo-x)*(xo-x) + (yo-y)*(yo-y) + (zo-z)*(zo-z);
+
+        if( currError<minError )
+          {
+          minError = currError;
+          minErrorIndex1 = i;
+          minErrorIndex2 = j;
+          }
+        }
+      }
+
+    if( minError< 0.05f ) // TODO: 0.05 ?
+      {
+      pos[offset  ] = mOrigPos[minErrorIndex1][3*minErrorIndex2  ];
+      pos[offset+1] = mOrigPos[minErrorIndex1][3*minErrorIndex2+1];
+      pos[offset+2] = mOrigPos[minErrorIndex1][3*minErrorIndex2+2];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void applyScrambles(int[][] moves)
+    {
+    setupPosition(moves);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void initializeObject(int[][] moves)
+    {
+    solve();
+    setupPosition(moves);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void recomputeFaceOffsets()
+    {
+    if( mThereAreDeciders )
+      {
+      int len = mRowOffsets.length;
+
+      for(int i=0; i<len; i++)
+        {
+        mRowOffsets[i][0] =0;
+        mRowOffsets[i][1] =0;
+        mRowOffsets[i][2] =0;
+        }
+
+      for(int i=0; i<mNumCubits; i++)
+        if( mCubits[i].getType()==TwistyObjectCubit.TYPE_DECIDER )
+          {
+          float[] offset = mCubits[i].getOffset();
+          int face = mCubits[i].getPuzzleFace();
+          mRowOffsets[face][0] = offset[0];
+          mRowOffsets[face][1] = offset[1];
+          mRowOffsets[face][2] = offset[2];
+          }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void releaseResources()
+    {
+    for(int j=0; j<mNumCubits; j++) mCubits[j].releaseResources();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void restorePreferences(String key, OperatingSystemInterface os)
+    {
+    boolean error = false;
+
+    for(int i=0; i<mNumCubits; i++)
+      {
+      int quatIndex = mCubits[i].restorePreferences(key,os);
+
+      if( quatIndex>=0 && quatIndex<mNumQuats )
+        mCubits[i].rotateCubit(mObjectQuats[quatIndex],true);
+
+      else { error = true; break; }
+      }
+
+    if( !error )
+      {
+      recomputeFaceOffsets();
+
+      for(int i=0; i<mNumCubits; i++)
+        {
+        int quatIndex = mCubits[i].mQuatIndex;
+
+        if( quatIndex>=0 && quatIndex<mNumQuats )
+          mCubits[i].computeRotationRow();
+
+        else { error = true; break; }
+        }
+      }
+
+    if( error )
+      {
+      for(int i=0; i<mNumCubits; i++) mCubits[i].solve();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void savePreferences(String key, OperatingSystemInterface os)
+    {
+    for(int i=0; i<mNumCubits; i++) mCubits[i].savePreferences(key,os);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void removePreferences(String key, OperatingSystemInterface os)
+    {
+    for(int i=0; i<mNumCubits; i++) mCubits[i].removePreferences(key,os);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static4D[] getQuats()
+    {
+    if( mObjectQuats==null )
+      {
+      mObjectQuats = QuatGroupGenerator.computeGroup(mAxis,mBasicAngles);
+      }
+
+    return mObjectQuats;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// INTERNAL API - those are called from 'effects' package
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitType(int cubit)       { return mCubits[cubit].getType(); }
+  public float[] getCubitOffset(int cubit) { return mCubits[cubit].getOffset(); }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getNumLayers()
+    {
+    return mNumLayers;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void solve()
+    {
+    for(int i=0; i<mNumCubits; i++) mCubits[i].solve();
+    recomputeFaceOffsets();
+    for(int i=0; i<mNumCubits; i++) mCubits[i].computeRotationRow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitQuatIndex(int cubit)
+    {
+    return (cubit>=0 && cubit<mNumCubits) ? mCubits[cubit].mQuatIndex : 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCubitRotRow(int cubit, int axis)
+    {
+    return mCubits[cubit].getRotRow(axis);
+    }
+  }
+
diff --git a/src/main/java/org/distorted/objectlib/objects/TwistyCrazy3x3.java b/src/main/java/org/distorted/objectlib/objects/TwistyCrazy3x3.java
index 0faef975..5be1e9b8 100644
--- a/src/main/java/org/distorted/objectlib/objects/TwistyCrazy3x3.java
+++ b/src/main/java/org/distorted/objectlib/objects/TwistyCrazy3x3.java
@@ -98,7 +98,7 @@ public class TwistyCrazy3x3 extends ShapeHexahedron
   private boolean cubitIsOverriden(int cubit)
     {
     float[] offset = getCubitRowOffset(cubit);
-    return offset[0]*offset[0]+offset[1]*offset[1]+offset[2]*offset[2] == 0;
+    return offset[0]*offset[0] + offset[1]*offset[1] + offset[2]*offset[2] == 0;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
