commit 63bfcdd15b1ade4c2437cd6ffc8a7fd7aba8783b
Author: leszek <leszek@koltunski.pl>
Date:   Sat Dec 16 01:03:14 2023 +0100

    Support for shape-shifting cuboids. (except for the flat ones like the Super Floppy)

diff --git a/src/main/java/org/distorted/objectlib/json/JsonReader.java b/src/main/java/org/distorted/objectlib/json/JsonReader.java
index 657be8af..aa798b2c 100644
--- a/src/main/java/org/distorted/objectlib/json/JsonReader.java
+++ b/src/main/java/org/distorted/objectlib/json/JsonReader.java
@@ -88,6 +88,7 @@ public class JsonReader
   private float[][] mCubitRowOffset;
   private ObjectStickerOverride[] mStickerOverrides;
   private float mPillowCoeff;
+  private int[][] mMinimalCubitsInRow;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -547,6 +548,16 @@ public class JsonReader
         for(int j=0; j<numRota; j++) mRotationFactor[i][j] = 1.0f;
         }
 
+      JSONArray jsonMinimal = jsonAx.optJSONArray("minimal");
+
+      if( jsonMinimal!=null )
+        {
+        if( mMinimalCubitsInRow==null ) mMinimalCubitsInRow = new int[numAxis][];
+        int numMinimal = jsonMinimal.length();
+        mMinimalCubitsInRow[i] = new int[numMinimal];
+        for(int j=0; j<numMinimal; j++) mMinimalCubitsInRow[i][j] = jsonMinimal.getInt(j);
+        }
+
       mNumLayers[i] = numRota;
       }
     }
@@ -946,6 +957,13 @@ public class JsonReader
       }
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getMinimalCubiesInRow()
+    {
+    return mMinimalCubitsInRow;
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public int getScrambleType()
diff --git a/src/main/java/org/distorted/objectlib/json/JsonWriter.java b/src/main/java/org/distorted/objectlib/json/JsonWriter.java
index 1d849092..bc6310f7 100644
--- a/src/main/java/org/distorted/objectlib/json/JsonWriter.java
+++ b/src/main/java/org/distorted/objectlib/json/JsonWriter.java
@@ -34,7 +34,7 @@ import org.distorted.objectlib.main.TwistyObject;
 
 public class JsonWriter
 {
-  public static final int VERSION_OBJECT_APP = 13;
+  public static final int VERSION_OBJECT_APP = 14;
   public static final int VERSION_EXTRAS_APP = 1;
 
   private static JsonWriter mThis;
@@ -479,6 +479,7 @@ public class JsonWriter
     float[][] cuts = object.getCuts(numLayers);
     boolean[][] rotatable = object.getLayerRotatable(numLayers);
     float[][] rotFactor = object.returnRotationFactor();
+    int[][] minimalCubits = object.getMinimalCubiesInRow();
 
     for( int i=0; i<numAxis; i++ )
       {
@@ -502,6 +503,13 @@ public class JsonWriter
       for(float rotf : rotFactor[i]) factorArray.put(rotf);
       axObject.put("factor", factorArray );
 
+      if( minimalCubits!=null )
+        {
+        JSONArray minimalArray = new JSONArray();
+        for(int min : minimalCubits[i]) minimalArray.put(min);
+        axObject.put("minimal", minimalArray );
+        }
+
       axis.put(axObject);
       }
 
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectControl.java b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
index 7fe220c9..09dc0b29 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectControl.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
@@ -668,14 +668,6 @@ public class ObjectControl
       mPreRender.getObject().setObjectRatioNow(scale,mScalingSize );
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void multQuatNow(Static4D quat)
-      {
-      Static4D q = QuatHelper.quatMultiply(mQuat,quat);
-      mQuat.set(q);
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void setQuat()
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
index 023dfb99..2fb44a80 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
@@ -484,13 +484,6 @@ public class ObjectPreRender implements EffectListener
     mScale = scale;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void ghostAngleCompensation(Static4D quat)
-    {
-    mController.multQuatNow(quat);
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // INTERNAL API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java b/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java
index e31c56a5..8b55c394 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyLayerRotations.java
@@ -15,7 +15,6 @@ import org.distorted.library.message.EffectListener;
 import org.distorted.library.type.Dynamic1D;
 import org.distorted.library.type.Static1D;
 import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -42,13 +41,13 @@ public class TwistyLayerRotations
   private final int mGhostAngle;
   private final Static3D[] mAxis;
   private final int[] mNumLayers;
-
+  private int mSolvedBitmap;
   private int mGhostEffectBitmap;
   private VertexEffectRotate mGhostEffect1;
-  private int mRotCompensation, mAccumulatedCompensation;
   private boolean mCanGhost;
   private Static3D mEffectAx;
   private int mAxNum;
+  private int mGhostRot;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -80,7 +79,8 @@ public class TwistyLayerRotations
       int nL = mNumLayers[mAxNum];
       mEffectAx = mAxis[mAxNum];
       int numRightRows = nL - nL/2;
-      mGhostEffectBitmap = ((1<<numRightRows)-1)<<(nL/2);
+      mSolvedBitmap =  ((1<<numRightRows)-1)<<(nL/2);
+      mGhostEffectBitmap = mSolvedBitmap;
       mParent.enableGhostAxis(mAxNum,false);
       Static1D ghostAngle1= new Static1D(mGhostAngle);
       mGhostEffect1 = new VertexEffectRotate(ghostAngle1, mEffectAx, CENTER);
@@ -95,11 +95,20 @@ public class TwistyLayerRotations
 
   private int computeGhostAngle(int row, int basicAngle, float angle, float speed)
     {
-    int a = (int)((2*(angle<0 ? -angle : angle) - mGhostAngle)*basicAngle/360) + 1;
+    boolean ghosted = rowIsGhosted(row);
+    int basic = 360/basicAngle;
+    int nearest = computeNearest( ghosted ? basic-mGhostAngle : mGhostAngle, basic, angle );
+    if( (nearest%basic)!=0 )
+      {
+      changeRow(row);
+      mGhostRot = (ghosted ? 1:-1)*mGhostAngle;
+      }
 
-    android.util.Log.e("D","angle="+angle+" a="+a+" ghost: "+((a%2)==1) );
+    int ret= computeNoGhostAngle(basicAngle,angle,speed);
 
-    return computeNoGhostAngle(basicAngle,angle,speed);
+    android.util.Log.e("D", "computeGhostAngle: "+ret+" ghostRot="+mGhostRot+" angle>0: "+(angle>0)+" ghosted="+ghosted);
+
+    return ret;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -111,19 +120,22 @@ public class TwistyLayerRotations
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void changeCompensationAngle(int diff)
+  private int computeNearest(int ghostAngle, int basicAngle, float angle)
     {
-    mAccumulatedCompensation = diff;
+    float A = (2*angle-ghostAngle)/basicAngle;
+    int k = A>=0 ? (int)A : (int)A-1;
+    return (k%2)==0 ? (k/2)*basicAngle+ghostAngle : ((k+1)/2)*basicAngle;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void changeRow(int row)
     {
-    if( mGhostEffectBitmap==0 ) mParent.enableGhostAxis(mAxNum,false);
+    android.util.Log.e("D", "changing row "+row);
+    int nL = mNumLayers[mAxNum];
+    if( mGhostEffectBitmap==0 || mGhostEffectBitmap==((1<<nL)-1) ) mParent.enableGhostAxis(mAxNum,false);
     mGhostEffectBitmap ^= (1<<row);
-    if( mGhostEffectBitmap==0 ) mParent.enableGhostAxis(mAxNum,true);
-
+    if( mGhostEffectBitmap==0 || mGhostEffectBitmap==((1<<nL)-1) ) mParent.enableGhostAxis(mAxNum,true);
     mGhostEffect1.setMeshAssociation( mGhostEffectBitmap, -1);
     }
 
@@ -144,7 +156,7 @@ public class TwistyLayerRotations
 
   private int computeNearestAngle(int axisIndex, int row, int basicAngle, float angle, float speed)
     {
-    if( mGhostAngle==0 || axisIndex!=0 || !mCanGhost )
+    if( mGhostAngle==0 || axisIndex!=0 /*|| !mCanGhost*/ )
       {
       return computeNoGhostAngle(basicAngle,angle,speed);
       }
@@ -184,14 +196,11 @@ public class TwistyLayerRotations
 
   long finishRotation(ObjectPreRender pre, int axisIndex, int row, int basicAngle, float finishAngle, float avgSpeed)
     {
-    mRotCompensation = 0;
-    mAccumulatedCompensation = 0;
-
+    mGhostRot = 0;
     int nearestAngleInDegrees = computeNearestAngle(axisIndex,row,basicAngle,finishAngle,avgSpeed);
 
     mRotationState = STATE_FINISH;
-    float angle = getAngle() + mRotCompensation;  // ghosts: we 'hide' the GhostAngle inside the
-                                                  // mGhostEffect1; here we must compensate.
+    float angle = getAngle() + mGhostRot;
     mRotationAngleStatic.set0(angle);
     mRotationAngleFinal.set0(nearestAngleInDegrees);
     mRotationAngleMiddle.set0( nearestAngleInDegrees + (nearestAngleInDegrees-angle)*0.2f );
@@ -203,19 +212,6 @@ public class TwistyLayerRotations
     mRotationAngle.add(mRotationAngleFinal);
     mRotateEffect.notifyWhenFinished(pre);
 
-    if( mAccumulatedCompensation!=0 )
-      {
-      float half = (float)((Math.PI*mAccumulatedCompensation*mGhostAngle)/360);
-      float cos  = (float)(Math.cos(half));
-      float sin  = (float)(Math.sin(half));
-      Static3D ax = mAxis[0];
-      float x = ax.get0();
-      float y = ax.get1();
-      float z = ax.get2();
-      Static4D quat = new Static4D(x*sin,y*sin,z*sin,cos);
-      pre.ghostAngleCompensation(quat);
-      }
-
     return mRotateEffect.getID();
     }
 
@@ -230,7 +226,7 @@ public class TwistyLayerRotations
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // this is 'programmatic' rotation - i.e. for example the one induced by the 'backMove' button.
 
-  synchronized long addRotation(EffectListener listener, Static3D axis, int axisIndex, int rowBitmap, int angle, long durationMillis )
+  synchronized long addRotation(EffectListener listener, int axisIndex, int rowBitmap, int angle, long durationMillis )
     {
     int mult = 1;
 
@@ -241,7 +237,7 @@ public class TwistyLayerRotations
     mCurrentRotAxis= axisIndex;
     mRotRowBitmap  = mParent.computeBandagedBitmap(rowBitmap,axisIndex);
     mRotationAngleStatic.set0(0.0f);
-    mRotationAxis.set(axis);
+    mRotationAxis.set(mAxis[axisIndex]);
     mRotationAngle.setDuration(durationMillis);
     mRotationAngle.resetToBeginning();
     mRotationAngle.add(new Static1D(0));
@@ -256,7 +252,7 @@ public class TwistyLayerRotations
 // i.e. we just touched the screen, moved it a little bit and the TouchControl has figured out
 // along which axis we are going to rotate.
 
-  synchronized boolean beginRotation(Static3D axis, int axisIndex, int row )
+  synchronized boolean beginRotation(int axisIndex, int row )
     {
     if( mRotationState==STATE_ROTATE ) return false;
     if( mRotationState==STATE_FINISH ) removeRotation();
@@ -265,7 +261,7 @@ public class TwistyLayerRotations
     mCurrentRotAxis= axisIndex;
     mRotRowBitmap  = mParent.computeBandagedBitmap( (1<<row),axisIndex );
     mRotationAngleStatic.set0(0.0f);
-    mRotationAxis.set(axis);
+    mRotationAxis.set( mAxis[axisIndex] );
     mRotationAngle.add(mRotationAngleStatic);
     mRotateEffect.setMeshAssociation( mRotRowBitmap<<(axisIndex*mMaxNumLayers) , -1);
 
@@ -277,6 +273,6 @@ public class TwistyLayerRotations
   boolean isSolved(boolean solved)
     {
     mCanGhost = solved;
-    return (solved && (mGhostAngle==0 || mGhostEffectBitmap==0) );
+    return (solved && (mGhostAngle==0 || mGhostEffectBitmap==mSolvedBitmap) );
     }
   }
diff --git a/src/main/java/org/distorted/objectlib/main/TwistyObject.java b/src/main/java/org/distorted/objectlib/main/TwistyObject.java
index 272b325e..4c521408 100644
--- a/src/main/java/org/distorted/objectlib/main/TwistyObject.java
+++ b/src/main/java/org/distorted/objectlib/main/TwistyObject.java
@@ -103,6 +103,7 @@ public abstract class TwistyObject
   private int mNumCubitFaces, mNumStickerTypes;
   private Static3D[] mAxis;
   private float[][] mCuts;
+  private int[][] mMinimalCubiesInRow;
   private int[] mNumCuts;
   private float[][] mOrigPos;
   private Static4D[] mOrigQuat;
@@ -224,6 +225,7 @@ public abstract class TwistyObject
       mNumCuts[i] = (mCuts==null || mCuts[i]==null ? 0 : mCuts[i].length);
       }
 
+    mMinimalCubiesInRow = getMinimalCubiesInRow();
     mNumCubits = mOrigPos.length;
     mNumFaceColors = getNumFaceColors();
     mBelongs = new boolean[mNumCubits];
@@ -968,6 +970,13 @@ public abstract class TwistyObject
     return ObjectScrambler.SCRAMBLING_ALGORITHMS;
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[][] getMinimalCubiesInRow()
+    {
+    return null;
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   int computeBandagedBitmap(int rowBitmap, int axis)
@@ -988,6 +997,53 @@ public abstract class TwistyObject
         }
       }
 
+    if( mMinimalCubiesInRow!=null )
+      {
+      int[] minC = mMinimalCubiesInRow[axis];
+      int numL = minC.length;
+      int[] numC = new int[numL];
+
+      for(int cubit=0; cubit<mNumCubits; cubit++)
+        {
+        int bitmap = mCubits[cubit].getRotRow(axis);
+
+        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;
     }
 
@@ -1124,7 +1180,7 @@ public abstract class TwistyObject
 
   synchronized long addRotation(EffectListener listener, int axis, int rowBitmap, int angle, long durationMillis )
     {
-    return mRotation.addRotation(listener,mAxis[axis],axis,rowBitmap,angle,durationMillis);
+    return mRotation.addRotation(listener,axis,rowBitmap,angle,durationMillis);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1143,13 +1199,8 @@ public abstract class TwistyObject
       android.util.Log.e("object", "invalid rotation axis: "+axis);
       return false;
       }
-    if( row<0 || row>=mNumLayers[axis] )
-      {
-      android.util.Log.e("object", "invalid rotation row: "+row);
-      return false;
-      }
 
-    return mRotation.beginRotation(mAxis[axis],axis,row);
+    return mRotation.beginRotation(axis,row);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java
index e8dfa6eb..864ef8a2 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlBall.java
@@ -366,6 +366,6 @@ public class TouchControlBall extends TouchControl
 
   public void enableGhostAxis(int axNum, boolean enable)
     {
-    mGhostAxisEnabled = enable ? axNum : -1;
+    mGhostAxisEnabled = enable ? -1 : axNum;
     }
 }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java
index b82274df..680ff6e8 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeChanging.java
@@ -622,6 +622,6 @@ public class TouchControlShapeChanging extends TouchControl
 
   public void enableGhostAxis(int axNum, boolean enable)
     {
-    mGhostAxisEnabled = enable ? axNum : -1;
+    mGhostAxisEnabled = enable ? -1 : axNum;
     }
   }
diff --git a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java
index 9490d249..83c7724a 100644
--- a/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java
+++ b/src/main/java/org/distorted/objectlib/touchcontrol/TouchControlShapeConstant.java
@@ -468,6 +468,6 @@ public abstract class TouchControlShapeConstant extends TouchControl
 
   public void enableGhostAxis(int axNum, boolean enable)
     {
-    mGhostAxisEnabled = enable ? axNum : -1;
+    mGhostAxisEnabled = enable ? -1 : axNum;
     }
   }
