commit 4c2c0f44c6cff280c0a666cae770439c4f2d0bbd
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Wed Mar 1 23:36:23 2023 +0100

    New button in the solver screen: reset all textures.

diff --git a/src/main/java/org/distorted/objectlib/effects/BaseEffect.java b/src/main/java/org/distorted/objectlib/effects/BaseEffect.java
index a6e569be..a87ac97d 100644
--- a/src/main/java/org/distorted/objectlib/effects/BaseEffect.java
+++ b/src/main/java/org/distorted/objectlib/effects/BaseEffect.java
@@ -15,6 +15,7 @@ import android.content.SharedPreferences;
 
 import org.distorted.objectlib.effects.fastscramble.FastScrambleEffect;
 import org.distorted.objectlib.effects.present.PresentEffect;
+import org.distorted.objectlib.effects.resticker.RestickerEffect;
 import org.distorted.objectlib.effects.scramble.ScrambleEffect;
 import org.distorted.objectlib.effects.objectchange.ObjectChangeEffect;
 import org.distorted.objectlib.effects.solve.SolveEffect;
@@ -34,6 +35,7 @@ public class BaseEffect
     FAST_SCRAMBLE ( 20, 0, R.string.fast_scramble_effect, FastScrambleEffect.class),
     PRESENT       (100, 0, R.string.present_effect, PresentEffect.class),
     WIN           ( 20, 1, R.string.win_effect          , WinEffect.class         ),
+    RESTICKER     ( 20, 0, R.string.resticker_effect          , RestickerEffect.class         ),
     ;
 
     private final int mDefaultPos, mDefaultType;
diff --git a/src/main/java/org/distorted/objectlib/effects/resticker/RestickerEffect.java b/src/main/java/org/distorted/objectlib/effects/resticker/RestickerEffect.java
new file mode 100644
index 00000000..7c587f7e
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/effects/resticker/RestickerEffect.java
@@ -0,0 +1,231 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 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.effects.resticker;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.message.EffectListener;
+import org.distorted.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectPreRender;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objectlib.main.TwistyObjectNode;
+
+import java.lang.reflect.Method;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class RestickerEffect extends BaseEffect implements EffectListener
+{
+  public enum Type
+    {
+    SPIN   (RestickerEffectSpin.class),
+    ;
+
+    final Class<? extends RestickerEffect> effect;
+
+    Type(Class<? extends RestickerEffect> effect)
+      {
+      this.effect = effect;
+      }
+    }
+
+  private static final int NUM_EFFECTS = Type.values().length;
+  private static final int NUM_PHASES  = 2;
+  private static final int FAKE_EFFECT_ID = -2;
+  private static final Type[] types;
+
+  static
+    {
+    int i=0;
+    types = new Type[NUM_EFFECTS];
+
+    for(Type type: Type.values())
+      {
+      types[i++] = type;
+      }
+    }
+
+  private int mDuration;
+  private int mEffectReturned;
+  private final int[] mCubeEffectNumber, mNodeEffectNumber;
+  private int mPhase;
+
+  ObjectPreRender mPre;
+  TwistyObject mObject;
+  TwistyObjectNode mObjectNode;
+  Effect[][] mCubeEffects;
+  int[][] mCubeEffectPosition;
+  Effect[][] mNodeEffects;
+  int[][] mNodeEffectPosition;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  RestickerEffect()
+    {
+    mPhase= 0;
+
+    mCubeEffectNumber   = new int[NUM_PHASES];
+    mNodeEffectNumber   = new int[NUM_PHASES];
+    mCubeEffectPosition = new int[NUM_PHASES][];
+    mNodeEffectPosition = new int[NUM_PHASES][];
+    mCubeEffects        = new Effect[NUM_PHASES][];
+    mNodeEffects        = new Effect[NUM_PHASES][];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract void createEffectsPhase0(int duration);
+  abstract void createEffectsPhase1(int duration);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void assignEffects(int phase)
+    {
+    mCubeEffectNumber[phase] = ( mCubeEffects[phase]!=null ) ? mCubeEffects[phase].length : 0;
+    mNodeEffectNumber[phase] = ( mNodeEffects[phase]!=null ) ? mNodeEffects[phase].length : 0;
+
+    if( mCubeEffectNumber[phase]==0 && mNodeEffectNumber[phase]==0 )
+      {
+      throw new RuntimeException("Cube and Node Effects ("+phase+" phase) both not created!");
+      }
+
+    for(int i=0; i<mCubeEffectNumber[phase]; i++)
+      {
+      mObject.applyEffect(mCubeEffects[phase][i],mCubeEffectPosition[phase][i]);
+      mCubeEffects[phase][i].notifyWhenFinished(this);
+      }
+
+    DistortedEffects nodeEffects = mObjectNode.getEffects();
+
+    for(int i=0; i<mNodeEffectNumber[phase]; i++)
+      {
+      nodeEffects.apply(mNodeEffects[phase][i],mNodeEffectPosition[phase][i]);
+      mNodeEffects[phase][i].notifyWhenFinished(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void effectAction(int phase)
+    {
+    switch(phase)
+      {
+      case 0: mEffectReturned = 0;
+              mPhase          = 1;
+              mPre.resetAllTextureMaps();
+              createEffectsPhase1(mDuration);
+              assignEffects(mPhase);
+              break;
+      case 1: mPre.effectFinished(FAKE_EFFECT_ID);
+              break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static String[] getNames()
+    {
+    String[] names = new String[NUM_EFFECTS];
+
+    for( int i=0; i<NUM_EFFECTS; i++)
+      {
+      names[i] = types[i].name();
+      }
+
+    return names;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static RestickerEffect create(int ordinal) throws InstantiationException, IllegalAccessException
+    {
+    return types[ordinal].effect.newInstance();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void effectFinished(final long effectID)
+    {
+    int total = mCubeEffectNumber[mPhase]+mNodeEffectNumber[mPhase];
+
+    for(int i=0; i<mCubeEffectNumber[mPhase]; i++)
+      {
+      long id = mCubeEffects[mPhase][i].getID();
+
+      if( effectID == id )
+        {
+        if( ++mEffectReturned == total ) effectAction(mPhase);
+        mObject.removeEffect(id);
+        return;
+        }
+      }
+    for(int i=0; i<mNodeEffectNumber[mPhase]; i++)
+      {
+      long id = mNodeEffects[mPhase][i].getID();
+
+      if( effectID == id )
+        {
+        if( ++mEffectReturned == total ) effectAction(mPhase);
+        mObjectNode.getEffects().abortById(id);
+        return;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public long start(int duration, ObjectPreRender pre)
+    {
+    mObject     = pre.getObject();
+    mObjectNode = pre.getObjectNode();
+    mPre        = pre;
+    mDuration   = duration;
+
+    createEffectsPhase0(mDuration);
+    assignEffects(mPhase);
+
+    return FAKE_EFFECT_ID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  public static void enableEffects()
+    {
+    Method method;
+
+    for(Type type: Type.values())
+      {
+      try
+        {
+        method = type.effect.getDeclaredMethod("enable"); // enable not public, thus getDeclaredMethod
+        }
+      catch(NoSuchMethodException ex)
+        {
+        android.util.Log.e("RestickerEffect", type.effect.getSimpleName()+": exception getting method: "+ex.getMessage());
+        method = null;
+        }
+
+      try
+        {
+        if( method!=null ) method.invoke(null);
+        }
+      catch(Exception ex)
+        {
+        android.util.Log.e("RestickerEffect", type.effect.getSimpleName()+": exception invoking method: "+ex.getMessage());
+        }
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/objectlib/effects/resticker/RestickerEffectSpin.java b/src/main/java/org/distorted/objectlib/effects/resticker/RestickerEffectSpin.java
new file mode 100644
index 00000000..547cd1c0
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/effects/resticker/RestickerEffectSpin.java
@@ -0,0 +1,75 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.effects.resticker;
+
+import static org.distorted.library.main.QuatHelper.rotateVectorByInvertedQuat;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.MatrixEffectRotate;
+import org.distorted.library.type.Dynamic;
+import org.distorted.library.type.Dynamic1D;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RestickerEffectSpin extends RestickerEffect
+  {
+  private void createEffects(int phase, int duration, int[] points)
+    {
+    mCubeEffectPosition[phase] = new int[] {0};
+    mCubeEffects[phase]        = new Effect[mCubeEffectPosition[0].length];
+
+    Static4D quaternion = mObject.getRotationQuat();                      // always rotate around
+    Static4D tmpAxis    = new Static4D(0,-1,0,0);                         // vert axis no matter
+    Static4D rotated    = rotateVectorByInvertedQuat(tmpAxis,quaternion); // how cube is rotated
+
+    Static3D axis  = new Static3D(rotated.get0(), rotated.get1(), rotated.get2());
+    Static3D center= new Static3D(0,0,0);
+
+    Dynamic1D d = new Dynamic1D(duration/2, 1.0f);
+    d.setMode(Dynamic.MODE_JUMP);
+    d.setConvexity(0.0f);   // otherwise speed of the rotation would be strangely uneven
+
+    d.add( new Static1D(36*points[0]) );
+    d.add( new Static1D(36*points[1]) );
+    d.add( new Static1D(36*points[2]) );
+    d.add( new Static1D(36*points[3]) );
+    d.add( new Static1D(36*points[4]) );
+
+    mCubeEffects[phase][0] = new MatrixEffectRotate(d,axis,center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void createEffectsPhase0(int duration)
+    {
+    createEffects(0,duration,new int[] {0,1,3,6,10});
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void createEffectsPhase1(int duration)
+    {
+    createEffects(1,duration,new int[] {0,4,7,9,10});
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Enable all effects used in this Effect. Called by reflection from the parent class.
+
+  @SuppressWarnings("unused")
+  static void enable()
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectControl.java b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
index 86e30881..606953f3 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectControl.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
@@ -764,6 +764,13 @@ public class ObjectControl
       mPreRender.solveOnly();
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void resetTextureMapsEffect(int duration)
+      {
+      mPreRender.resetTextureMapsEffect(duration);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void addRotation(MovesFinished listener, int axis, int rowBitmap, int angle, int duration)
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
index 459eee7a..d79bce53 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
@@ -47,8 +47,9 @@ public class ObjectPreRender implements EffectListener
   private float mScale;
 
   private boolean mFinishRotation, mRemoveRotation, mRemovePatternRotation, mAddRotation,
-                  mSetQuat, mChangeObject, mSolveObject, mScrambleObject, mFastScrambleObject, mPresentObject,
-                  mInitializeObject, mSetTextureMap, mResetAllTextureMaps, mSolve, mApplyScrambles;
+                  mSetQuat, mChangeObject, mSolveObject, mScrambleObject, mFastScrambleObject,
+                  mPresentObject,mInitializeObject, mSetTextureMap, mResetAllTextureMaps, mSolve,
+                  mApplyScrambles, mResetTextureEffect;
   private boolean mScramblingAndSolvingBlocked, mRotationBlocked, mIsSolved;
   private long mRotationFinishedID;
   private final long[] mEffectID;
@@ -56,7 +57,7 @@ public class ObjectPreRender implements EffectListener
   private int mScrambleObjectNum, mScrambleObjectDuration;
   private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
   private long mAddRotationDuration;
-  private int mPresentDuration;
+  private int mPresentDuration, mRestickerDuration;
   private long mAddRotationID, mRemoveRotationID;
   private int mCubit, mFace, mNewColor;
   private int mNearestAngle;
@@ -84,6 +85,7 @@ public class ObjectPreRender implements EffectListener
     mScrambleObject       = false;
     mFastScrambleObject   = false;
     mPresentObject        = false;
+    mResetTextureEffect   = false;
 
     mOldObject = null;
     mNewObject = null;
@@ -308,6 +310,14 @@ public class ObjectPreRender implements EffectListener
     doEffectNow( BaseEffect.Type.FAST_SCRAMBLE, mScrambleObjectDuration );
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void resetTextureNow()
+    {
+    mResetTextureEffect = false;
+    doEffectNow( BaseEffect.Type.RESTICKER, mRestickerDuration );
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private void presentObjectNow()
@@ -604,6 +614,7 @@ public class ObjectPreRender implements EffectListener
     if( mApplyScrambles        ) applyScramblesNow();
     if( mResetAllTextureMaps   ) resetAllTextureMapsNow();
     if( mSetTextureMap         ) setTextureMapNow();
+    if( mResetTextureEffect    ) resetTextureNow();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -715,6 +726,14 @@ public class ObjectPreRender implements EffectListener
     mSolve = true;
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void resetTextureMapsEffect(int duration)
+    {
+    mRestickerDuration = duration;
+    mResetTextureEffect = true;
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void resetAllTextureMaps()
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 14149870..5a8dbfac 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -5,4 +5,5 @@
     <string name="fast_scramble_effect">Fast Scramble Effect</string>
     <string name="present_effect">Present Effect</string>
     <string name="win_effect">Win Effect</string>
+    <string name="resticker_effect">Resticker Effect</string>
 </resources>
