commit ee35e63c2ff0b9f49608f13a1cf8f8237f69b8ad
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Thu Sep 17 00:55:37 2020 +0100

    Beginnings of support for the Helicopter.

diff --git a/src/main/java/org/distorted/objects/RubikCube.java b/src/main/java/org/distorted/objects/RubikCube.java
index 4e665e39..056bfb73 100644
--- a/src/main/java/org/distorted/objects/RubikCube.java
+++ b/src/main/java/org/distorted/objects/RubikCube.java
@@ -23,8 +23,6 @@ import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
 import org.distorted.library.effect.VertexEffectDeform;
 import org.distorted.library.effect.VertexEffectMove;
 import org.distorted.library.effect.VertexEffectRotate;
diff --git a/src/main/java/org/distorted/objects/RubikDino.java b/src/main/java/org/distorted/objects/RubikDino.java
index d01f61c7..159a9bc1 100644
--- a/src/main/java/org/distorted/objects/RubikDino.java
+++ b/src/main/java/org/distorted/objects/RubikDino.java
@@ -537,7 +537,7 @@ public class RubikDino extends RubikObject
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// TODO  (only needed for solvers - there are no Dino solvers ATM)
+// only needed for solvers - there are no Dino solvers ATM)
 
   public String retObjectString()
     {
diff --git a/src/main/java/org/distorted/objects/RubikHelicopter.java b/src/main/java/org/distorted/objects/RubikHelicopter.java
new file mode 100644
index 00000000..76fde3b5
--- /dev/null
+++ b/src/main/java/org/distorted/objects/RubikHelicopter.java
@@ -0,0 +1,718 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objects;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.VertexEffectDeform;
+import org.distorted.library.effect.VertexEffectMove;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.effect.VertexEffectScale;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.mesh.MeshJoined;
+import org.distorted.library.mesh.MeshPolygon;
+import org.distorted.library.mesh.MeshSquare;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.RubikSurfaceView;
+
+import java.util.Random;
+
+import static org.distorted.effects.scramble.ScrambleEffect.START_AXIS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikHelicopter extends RubikObject
+{
+  private static final float SQ2 = (float)Math.sqrt(2);
+  private static final float SQ3 = (float)Math.sqrt(3);
+
+  private static final int FACES_PER_CUBIT =6;
+
+  // the six rotation axis of a Helicopter. Must be normalized.
+  static final Static3D[] ROT_AXIS = new Static3D[]
+         {
+           new Static3D(     0, +SQ2/2, -SQ2/2),
+           new Static3D(     0, -SQ2/2, -SQ2/2),
+           new Static3D(+SQ2/2,      0, -SQ2/2),
+           new Static3D(-SQ2/2,      0, -SQ2/2),
+           new Static3D(+SQ2/2, -SQ2/2,      0),
+           new Static3D(-SQ2/2, -SQ2/2,      0)
+         };
+
+  // the six axis that determine the faces
+  static final Static3D[] FACE_AXIS = new Static3D[]
+         {
+           new Static3D(1,0,0), new Static3D(-1,0,0),
+           new Static3D(0,1,0), new Static3D(0,-1,0),
+           new Static3D(0,0,1), new Static3D(0,0,-1)
+         };
+
+  private static final int[] FACE_COLORS = new int[]
+         {
+           0xffffff00, 0xffffffff,   // FACE_AXIS[0] (right-YELLOW) FACE_AXIS[1] (left  -WHITE)
+           0xff0000ff, 0xff00ff00,   // FACE_AXIS[2] (top  -BLUE  ) FACE_AXIS[3] (bottom-GREEN)
+           0xffff0000, 0xffb5651d    // FACE_AXIS[4] (front-RED   ) FACE_AXIS[5] (back  -BROWN)
+         };
+
+  // All legal rotation quats of a HELICOPTER (same as the Cube!)
+  private static final Static4D[] QUATS = new Static4D[]
+         {
+           new Static4D( 0.00f,  0.00f,  0.00f,  1.00f ),
+           new Static4D( 1.00f,  0.00f,  0.00f,  0.00f ),
+           new Static4D( 0.00f,  1.00f,  0.00f,  0.00f ),
+           new Static4D( 0.00f,  0.00f,  1.00f,  0.00f ),
+
+           new Static4D( SQ2/2,  SQ2/2,  0.00f,  0.00f ),
+           new Static4D( SQ2/2, -SQ2/2,  0.00f,  0.00f ),
+           new Static4D( SQ2/2,  0.00f,  SQ2/2,  0.00f ),
+           new Static4D( SQ2/2,  0.00f, -SQ2/2,  0.00f ),
+           new Static4D( SQ2/2,  0.00f,  0.00f,  SQ2/2 ),
+           new Static4D( SQ2/2,  0.00f,  0.00f, -SQ2/2 ),
+           new Static4D( 0.00f,  SQ2/2,  SQ2/2,  0.00f ),
+           new Static4D( 0.00f,  SQ2/2, -SQ2/2,  0.00f ),
+           new Static4D( 0.00f,  SQ2/2,  0.00f,  SQ2/2 ),
+           new Static4D( 0.00f,  SQ2/2,  0.00f, -SQ2/2 ),
+           new Static4D( 0.00f,  0.00f,  SQ2/2,  SQ2/2 ),
+           new Static4D( 0.00f,  0.00f,  SQ2/2, -SQ2/2 ),
+
+           new Static4D( 0.50f,  0.50f,  0.50f,  0.50f ),
+           new Static4D( 0.50f,  0.50f,  0.50f, -0.50f ),
+           new Static4D( 0.50f,  0.50f, -0.50f,  0.50f ),
+           new Static4D( 0.50f,  0.50f, -0.50f, -0.50f ),
+           new Static4D( 0.50f, -0.50f,  0.50f,  0.50f ),
+           new Static4D( 0.50f, -0.50f,  0.50f, -0.50f ),
+           new Static4D( 0.50f, -0.50f, -0.50f,  0.50f ),
+           new Static4D( 0.50f, -0.50f, -0.50f, -0.50f )
+         };
+
+  private static final float DIST_CORNER = 0.50f;
+  private static final float DIST_CENTER = 0.49f;
+  private static final float XY_CENTER   = DIST_CORNER/3;
+
+  // centers of the 8 corners + 6*4 face triangles ( i.e. of the all 32 cubits)
+  private static final Static3D[] CENTERS = new Static3D[]
+         {
+           new Static3D(   DIST_CORNER,   DIST_CORNER,   DIST_CORNER ),
+           new Static3D(   DIST_CORNER,   DIST_CORNER,  -DIST_CORNER ),
+           new Static3D(   DIST_CORNER,  -DIST_CORNER,   DIST_CORNER ),
+           new Static3D(   DIST_CORNER,  -DIST_CORNER,  -DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,   DIST_CORNER,   DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,   DIST_CORNER,  -DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,  -DIST_CORNER,   DIST_CORNER ),
+           new Static3D(  -DIST_CORNER,  -DIST_CORNER,  -DIST_CORNER ),
+
+           new Static3D(   DIST_CENTER,     XY_CENTER,     XY_CENTER ),
+           new Static3D(   DIST_CENTER,     XY_CENTER,    -XY_CENTER ),
+           new Static3D(   DIST_CENTER,    -XY_CENTER,     XY_CENTER ),
+           new Static3D(   DIST_CENTER,    -XY_CENTER,    -XY_CENTER ),
+
+           new Static3D(  -DIST_CENTER,     XY_CENTER,     XY_CENTER ),
+           new Static3D(  -DIST_CENTER,     XY_CENTER,    -XY_CENTER ),
+           new Static3D(  -DIST_CENTER,    -XY_CENTER,     XY_CENTER ),
+           new Static3D(  -DIST_CENTER,    -XY_CENTER,    -XY_CENTER ),
+
+           new Static3D(   XY_CENTER  ,   DIST_CENTER,     XY_CENTER ),
+           new Static3D(   XY_CENTER  ,   DIST_CENTER,    -XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,   DIST_CENTER,     XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,   DIST_CENTER,    -XY_CENTER ),
+
+           new Static3D(   XY_CENTER  ,  -DIST_CENTER,     XY_CENTER ),
+           new Static3D(   XY_CENTER  ,  -DIST_CENTER,    -XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,  -DIST_CENTER,     XY_CENTER ),
+           new Static3D(  -XY_CENTER  ,  -DIST_CENTER,    -XY_CENTER ),
+
+           new Static3D(   XY_CENTER  ,     XY_CENTER,   DIST_CENTER ),
+           new Static3D(   XY_CENTER  ,    -XY_CENTER,   DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,     XY_CENTER,   DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,    -XY_CENTER,   DIST_CENTER ),
+
+           new Static3D(   XY_CENTER  ,     XY_CENTER,  -DIST_CENTER ),
+           new Static3D(   XY_CENTER  ,    -XY_CENTER,  -DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,     XY_CENTER,  -DIST_CENTER ),
+           new Static3D(  -XY_CENTER  ,    -XY_CENTER,  -DIST_CENTER ),
+         };
+
+  // Colors of the faces of cubits. Each cubit has 6 faces
+  private static final int[][] mFaceMap = new int[][]
+         {
+           { 4,2,0, 6,6,6 },
+           { 2,5,0, 6,6,6 },
+           { 3,4,0, 6,6,6 },
+           { 5,3,0, 6,6,6 },
+           { 1,2,4, 6,6,6 },
+           { 5,2,1, 6,6,6 },
+           { 4,3,1, 6,6,6 },
+           { 1,3,5, 6,6,6 },
+
+           { 0 , 6,6,6,6,6 },
+           { 0 , 6,6,6,6,6 },
+           { 0 , 6,6,6,6,6 },
+           { 0 , 6,6,6,6,6 },
+
+           { 1 , 6,6,6,6,6 },
+           { 1 , 6,6,6,6,6 },
+           { 1 , 6,6,6,6,6 },
+           { 1 , 6,6,6,6,6 },
+
+           { 2 , 6,6,6,6,6 },
+           { 2 , 6,6,6,6,6 },
+           { 2 , 6,6,6,6,6 },
+           { 2 , 6,6,6,6,6 },
+
+           { 3 , 6,6,6,6,6 },
+           { 3 , 6,6,6,6,6 },
+           { 3 , 6,6,6,6,6 },
+           { 3 , 6,6,6,6,6 },
+
+           { 4 , 6,6,6,6,6 },
+           { 4 , 6,6,6,6,6 },
+           { 4 , 6,6,6,6,6 },
+           { 4 , 6,6,6,6,6 },
+
+           { 5 , 6,6,6,6,6 },
+           { 5 , 6,6,6,6,6 },
+           { 5 , 6,6,6,6,6 },
+           { 5 , 6,6,6,6,6 },
+         };
+
+  private static MeshBase mCornerMesh, mFaceMesh;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  RubikHelicopter(int size, Static4D quat, DistortedTexture texture,
+                  MeshSquare mesh, DistortedEffects effects, int[][] moves, Resources res, int scrWidth)
+    {
+    super(size, 60, quat, texture, mesh, effects, moves, RubikObjectList.HELI, res, scrWidth);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createCornerMesh()
+    {
+    float D = 0.02f;
+    float E = 0.5f;
+    float F = SQ2/4;
+
+    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-2*D, D*0.25f,
+                       1.0f-4*D, D*0.35f,
+                       1.0f-8*D, D*0.6f,
+                       0.60f   , D*1.0f,
+                       0.30f   , D*1.375f,
+                       0.0f    , D*1.4f };
+
+    MeshBase[] meshes = new MeshBase[6];
+
+    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
+    meshes[0].setEffectAssociation(0,1,0);
+    meshes[1] = meshes[0].copy(true);
+    meshes[1].setEffectAssociation(0,2,0);
+    meshes[2] = meshes[0].copy(true);
+    meshes[2].setEffectAssociation(0,4,0);
+
+    float[] vertices1 = { -F,-1.0f/12, +F,-1.0f/12, 0,1.0f/6 };
+    float[] bands1 = { 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f };
+
+    meshes[3] = new MeshPolygon(vertices1,bands1,1,3);
+    meshes[3].setEffectAssociation(0,8,0);
+    meshes[4] = meshes[3].copy(true);
+    meshes[4].setEffectAssociation(0,16,0);
+    meshes[5] = meshes[3].copy(true);
+    meshes[5].setEffectAssociation(0,32,0);
+
+    mCornerMesh = new MeshJoined(meshes);
+
+    Static3D axisX  = new Static3D(1,0,0);
+    Static3D axisY  = new Static3D(0,1,0);
+    Static3D axis0  = new Static3D(-SQ2/2,0,SQ2/2);
+    Static3D axis1  = new Static3D(+SQ3/3,+SQ3/3,+SQ3/3);
+    Static1D angle1 = new Static1D(+90);
+    Static1D angle2 = new Static1D(-90);
+    Static1D angle3 = new Static1D(-135);
+    Static1D angle4 = new Static1D(90);
+    Static1D angle5 = new Static1D(120);
+    Static1D angle6 = new Static1D(240);
+    Static3D center1= new Static3D(0,0,0);
+    Static3D center2= new Static3D(-0.25f,-0.25f,-0.25f);
+    Static3D move1  = new Static3D(-E/4,-E/4,0);
+    Static3D move2  = new Static3D(-0.25f,(-1.0f/6)-0.25f,-0.25f);
+
+    float d0 =-0.04f;
+    float d1 = 0.04f;
+    float r0 = 0.15f;
+    float r1 = 0.10f;
+
+    Static3D vec0   = new Static3D(d0*(+SQ3/3),d0*(+SQ3/3),d0*(+SQ3/3));
+    Static3D vec1   = new Static3D(d1*(+SQ3/3),d1*(-SQ3/3),d1*(-SQ3/3));
+    Static3D vec2   = new Static3D(d1*(-SQ3/3),d1*(+SQ3/3),d1*(-SQ3/3));
+    Static3D vec3   = new Static3D(d1*(-SQ3/3),d1*(-SQ3/3),d1*(+SQ3/3));
+
+    Static1D radius = new Static1D(0.5f);
+
+    Static3D cent0  = new Static3D( 0.0f, 0.0f, 0.0f);
+    Static3D cent1  = new Static3D(-0.5f, 0.0f, 0.0f);
+    Static3D cent2  = new Static3D( 0.0f,-0.5f, 0.0f);
+    Static3D cent3  = new Static3D( 0.0f, 0.0f,-0.5f);
+
+    Static4D reg0   = new Static4D(0,0,0,r0);
+    Static4D reg1   = new Static4D(0,0,0,r1);
+
+    VertexEffectMove   effect0 = new VertexEffectMove(move1);
+    VertexEffectScale  effect1 = new VertexEffectScale(new Static3D(1,1,-1));
+    VertexEffectRotate effect2 = new VertexEffectRotate(angle1,axisX,center1);
+    VertexEffectRotate effect3 = new VertexEffectRotate(angle2,axisY,center1);
+    VertexEffectMove   effect4 = new VertexEffectMove(move2);
+    VertexEffectRotate effect5 = new VertexEffectRotate(angle1,axisX,center2);
+    VertexEffectRotate effect6 = new VertexEffectRotate(angle3,axisY,center2);
+    VertexEffectRotate effect7 = new VertexEffectRotate(angle4,axis0,center2);
+    VertexEffectRotate effect8 = new VertexEffectRotate(angle5,axis1,center2);
+    VertexEffectRotate effect9 = new VertexEffectRotate(angle6,axis1,center2);
+
+    VertexEffectDeform effect10= new VertexEffectDeform(vec0,radius,cent0,reg0);
+    VertexEffectDeform effect11= new VertexEffectDeform(vec1,radius,cent1,reg1);
+    VertexEffectDeform effect12= new VertexEffectDeform(vec2,radius,cent2,reg1);
+    VertexEffectDeform effect13= new VertexEffectDeform(vec3,radius,cent3,reg1);
+
+    effect0.setMeshAssociation( 7,-1);  // meshes 0,1,2
+    effect1.setMeshAssociation( 6,-1);  // meshes 1,2
+    effect2.setMeshAssociation( 2,-1);  // mesh 1
+    effect3.setMeshAssociation( 4,-1);  // mesh 2
+    effect4.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect5.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect6.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect7.setMeshAssociation(56,-1);  // meshes 3,4,5
+    effect8.setMeshAssociation(16,-1);  // mesh 4
+    effect9.setMeshAssociation(32,-1);  // mesh 5
+
+    effect10.setMeshAssociation(63,-1); // all meshes
+    effect11.setMeshAssociation(63,-1); // all meshes
+    effect12.setMeshAssociation(63,-1); // all meshes
+    effect13.setMeshAssociation(63,-1); // all meshes
+
+    mCornerMesh.apply(effect0);
+    mCornerMesh.apply(effect1);
+    mCornerMesh.apply(effect2);
+    mCornerMesh.apply(effect3);
+    mCornerMesh.apply(effect4);
+    mCornerMesh.apply(effect5);
+    mCornerMesh.apply(effect6);
+    mCornerMesh.apply(effect7);
+    mCornerMesh.apply(effect8);
+    mCornerMesh.apply(effect9);
+
+    mCornerMesh.apply(effect10);
+    mCornerMesh.apply(effect11);
+    mCornerMesh.apply(effect12);
+    mCornerMesh.apply(effect13);
+
+    mCornerMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createFaceMesh()
+    {
+    float D = 0.02f;
+    float E = 0.5f;
+    float F = SQ2/4;
+
+    float[] vertices0 = { -E+E/4,E/4, E/4,-E+E/4, E/4,E/4};
+
+    float[] bands0 = { 1.0f    , 0,
+                       1.0f-2*D, D*0.25f,
+                       1.0f-4*D, D*0.35f,
+                       1.0f-8*D, D*0.6f,
+                       0.60f   , D*1.0f,
+                       0.30f   , D*1.375f,
+                       0.0f    , D*1.4f };
+
+    MeshBase[] meshes = new MeshBase[6];
+
+    meshes[0] = new MeshPolygon(vertices0, bands0, 3, 3);
+    meshes[0].setEffectAssociation(0,1,0);
+
+    // TODO
+
+    mFaceMesh.mergeEffComponents();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float getScreenRatio()
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D[] getQuats()
+    {
+    return QUATS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumFaces()
+    {
+    return FACE_COLORS.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumStickerTypes()
+    {
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getNumCubitFaces()
+    {
+    return FACES_PER_CUBIT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static3D[] getCubitPositions(int size)
+    {
+    return CENTERS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// TODO
+
+  private int getQuatIndex(int cubit)
+    {
+    switch(cubit)
+      {
+      case  0:
+      case  1:
+      case  2:
+      case  3:
+      case  4:
+      case  5:
+      case  6:
+      case  7:
+      case  8:
+      case  9:
+      case 10:
+      case 11:
+      case 12:
+      case 13:
+      case 14:
+      case 15:
+      case 16:
+      case 17:
+      case 18:
+      case 19:
+      case 20:
+      case 21:
+      case 22:
+      case 23:
+      case 24:
+      case 25:
+      case 26:
+      case 27:
+      case 28:
+      case 29:
+      case 30:
+      case 31:
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  MeshBase createCubitMesh(int cubit)
+    {
+    MeshBase mesh;
+
+    if( cubit<8 )
+      {
+      if( mCornerMesh==null ) createCornerMesh();
+      mesh = mCornerMesh.copy(true);
+      }
+    else
+      {
+      if( mFaceMesh==null ) createFaceMesh();
+      mesh = mFaceMesh.copy(true);
+      }
+
+    int index = getQuatIndex(cubit);
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion( QUATS[index], new Static3D(0,0,0) );
+    mesh.apply(quat,0xffffffff,0);
+
+    return mesh;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getFaceColor(int cubit, int cubitface, int size)
+    {
+    return mFaceMap[cubit][cubitface];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createFaceTexture(Canvas canvas, Paint paint, int face, int left, int top, int side)
+    {
+    float STROKE = 0.035f*side;
+    float L= left+0.125f*side;
+    float H= 0.375f*side;
+    float LEN = 0.5f*side;
+
+    paint.setAntiAlias(true);
+    paint.setStrokeWidth(STROKE);
+    paint.setColor(FACE_COLORS[face]);
+    paint.setStyle(Paint.Style.FILL);
+
+    canvas.drawRect(left,top,left+side,top+side,paint);
+
+    paint.setColor(INTERIOR_COLOR);
+    paint.setStyle(Paint.Style.STROKE);
+
+    canvas.drawLine( L    , H,  L+LEN, H    , paint);
+    canvas.drawLine( L    , H,  L+LEN, H+LEN, paint);
+    canvas.drawLine( L+LEN, H,  L+LEN, H+LEN, paint);
+
+    float S1 = 0.125f*side;
+    float S2 = 0.070f*side;
+    float X  = 0.7f*S2;
+
+    float LA = left+0.625f*side;
+    float RA = left+0.125f*side;
+    float TA = 0.375f*side;
+    float BA = 0.875f*side;
+
+    canvas.drawArc( LA-S1, TA     , LA     , TA+S1, 270, 90, false, paint);
+    canvas.drawArc( RA+X , TA     , RA+X+S2, TA+S2, 135,135, false, paint);
+    canvas.drawArc( LA-S2, BA-X-S2, LA     , BA-X ,   0,135, false, paint);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float returnMultiplier()
+    {
+    return 2.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getRowChances()
+    {
+    float[] chances = new float[4];
+
+    chances[0] = 0.5f;
+    chances[1] = 0.5f;
+    chances[2] = 0.5f;
+    chances[3] = 1.0f;
+
+    return chances;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public Static3D[] getRotationAxis()
+    {
+    return ROT_AXIS;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getBasicAngle()
+    {
+    return 2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int computeRowFromOffset(float offset)
+    {
+    return offset<0.25f ? 0:1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public float returnRotationFactor(float offset)
+    {
+    return 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRotAxis(Random rnd, int oldRotAxis)
+    {
+    int numAxis = ROTATION_AXIS.length;
+
+    if( oldRotAxis == START_AXIS )
+      {
+      return rnd.nextInt(numAxis);
+      }
+    else
+      {
+      int newVector = rnd.nextInt(numAxis-2);
+
+      switch(oldRotAxis)
+        {
+        case  0:
+        case  1: return newVector+2;
+        case  2:
+        case  3: return (newVector==0 || newVector==1) ? newVector:newVector+2;
+        default: return newVector;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int randomizeNewRow(Random rnd, int oldRotAxis, int oldRow, int newRotAxis)
+    {
+    float rowFloat = rnd.nextFloat();
+
+    for(int row=0; row<mRowChances.length; row++)
+      {
+      if( rowFloat<=mRowChances[row] ) return row;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// remember about the double cover or unit quaternions!
+
+  private int mulQuat(int q1, int q2)
+    {
+    Static4D result = RubikSurfaceView.quatMultiply(QUATS[q1],QUATS[q2]);
+
+    float rX = result.get0();
+    float rY = result.get1();
+    float rZ = result.get2();
+    float rW = result.get3();
+
+    final float MAX_ERROR = 0.1f;
+    float dX,dY,dZ,dW;
+
+    for(int i=0; i<QUATS.length; i++)
+      {
+      dX = QUATS[i].get0() - rX;
+      dY = QUATS[i].get1() - rY;
+      dZ = QUATS[i].get2() - rZ;
+      dW = QUATS[i].get3() - rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+
+      dX = QUATS[i].get0() + rX;
+      dY = QUATS[i].get1() + rY;
+      dZ = QUATS[i].get2() + rZ;
+      dW = QUATS[i].get3() + rW;
+
+      if( dX<MAX_ERROR && dX>-MAX_ERROR &&
+          dY<MAX_ERROR && dY>-MAX_ERROR &&
+          dZ<MAX_ERROR && dZ>-MAX_ERROR &&
+          dW<MAX_ERROR && dW>-MAX_ERROR  ) return i;
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// The Helicopter is solved if and only if:
+//
+// 1) all of its corner cubits are rotated with the same quat
+// 2) all its face cubits are rotated with the same quat like the corner ones,
+//    and optionally they also might be turned by a multiple of 90 degrees along
+//    a vector perpendicular to the face they lie on.
+//
+// i.e.
+// cubits  8, 9,10,11,12,13,14,15 - might be extra QUAT 1,8,9
+// cubits 16,17,18,19,20,21,22,23 - might be extra QUAT 2,12,13
+// cubits 24,25,26,27,28,29,30,31 - might be extra QUAT 3,14,15
+
+  public boolean isSolved()
+    {
+    int q = CUBITS[0].mQuatIndex;
+
+    if ( CUBITS[1].mQuatIndex == q &&
+         CUBITS[2].mQuatIndex == q &&
+         CUBITS[3].mQuatIndex == q &&
+         CUBITS[4].mQuatIndex == q &&
+         CUBITS[5].mQuatIndex == q &&
+         CUBITS[6].mQuatIndex == q &&
+         CUBITS[7].mQuatIndex == q  )
+      {
+      int q1 = mulQuat(q,1);
+      int q2 = mulQuat(q,8);
+      int q3 = mulQuat(q,9);
+
+      for(int index=8; index<16; index++)
+        {
+        int qIndex = CUBITS[index].mQuatIndex;
+        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
+        }
+
+      q1 = mulQuat(q, 2);
+      q2 = mulQuat(q,12);
+      q3 = mulQuat(q,13);
+
+      for(int index=16; index<24; index++)
+        {
+        int qIndex = CUBITS[index].mQuatIndex;
+        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
+        }
+
+      q1 = mulQuat(q, 3);
+      q2 = mulQuat(q,14);
+      q3 = mulQuat(q,15);
+
+      for(int index=24; index<32; index++)
+        {
+        int qIndex = CUBITS[index].mQuatIndex;
+        if( qIndex!=q && qIndex!=q1 && qIndex!=q2 && qIndex!=q3 ) return false;
+        }
+
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// only needed for solvers - there are no Helicopter solvers ATM)
+
+  public String retObjectString()
+    {
+    return "";
+    }
+
+}
diff --git a/src/main/java/org/distorted/objects/RubikMovementHelicopter.java b/src/main/java/org/distorted/objects/RubikMovementHelicopter.java
new file mode 100644
index 00000000..020883c4
--- /dev/null
+++ b/src/main/java/org/distorted/objects/RubikMovementHelicopter.java
@@ -0,0 +1,116 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objects;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class RubikMovementHelicopter extends RubikMovement
+{
+  RubikMovementHelicopter()
+    {
+    super(RubikSkewb.ROT_AXIS, RubikSkewb.FACE_AXIS, 0.25f, 0.25f);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// _____________
+// |  \  0  /  |
+// |   \   /   |
+// | 3 |   | 1 |
+// |   /   \   |
+// |  /  2  \  |
+// -------------
+
+  private int getQuarter(float[] touchPoint)
+    {
+    boolean p0 = touchPoint[1] >= touchPoint[0];
+    boolean p1 = touchPoint[1] >=-touchPoint[0];
+
+    if( p0 )  return p1 ? 0:3;
+    else      return p1 ? 1:2;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  boolean isInsideFace(float[] p)
+    {
+    return ( p[0]<=0.25f && p[0]>=-0.25f && p[1]<=0.25f && p[1]>=-0.25f );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void computeEnabledAxis(int face, float[] touchPoint, int[] enabled)
+    {
+    enabled[0] = 2;
+
+    int quarter = getQuarter(touchPoint);
+
+    switch(face)
+      {
+      case 0: switch(quarter)
+                {
+                case 0: enabled[1]=0; enabled[2]=1; break;
+                case 1: enabled[1]=3; enabled[2]=1; break;
+                case 2: enabled[1]=2; enabled[2]=3; break;
+                case 3: enabled[1]=0; enabled[2]=2; break;
+                }
+              break;
+      case 1: switch(quarter)
+                {
+                case 0: enabled[1]=2; enabled[2]=3; break;
+                case 1: enabled[1]=3; enabled[2]=1; break;
+                case 2: enabled[1]=0; enabled[2]=1; break;
+                case 3: enabled[1]=0; enabled[2]=2; break;
+                }
+              break;
+      case 2: switch(quarter)
+                {
+                case 0: enabled[1]=1; enabled[2]=2; break;
+                case 1: enabled[1]=0; enabled[2]=1; break;
+                case 2: enabled[1]=0; enabled[2]=3; break;
+                case 3: enabled[1]=2; enabled[2]=3; break;
+                }
+              break;
+      case 3: switch(quarter)
+                {
+                case 0: enabled[1]=1; enabled[2]=2; break;
+                case 1: enabled[1]=2; enabled[2]=3; break;
+                case 2: enabled[1]=0; enabled[2]=3; break;
+                case 3: enabled[1]=0; enabled[2]=1; break;
+                }
+              break;
+      case 4: switch(quarter)
+                {
+                case 0: enabled[1]=0; enabled[2]=3; break;
+                case 1: enabled[1]=0; enabled[2]=2; break;
+                case 2: enabled[1]=1; enabled[2]=2; break;
+                case 3: enabled[1]=1; enabled[2]=3; break;
+                }
+              break;
+      case 5: switch(quarter)
+                {
+                case 0: enabled[1]=1; enabled[2]=2; break;
+                case 1: enabled[1]=0; enabled[2]=2; break;
+                case 2: enabled[1]=0; enabled[2]=3; break;
+                case 3: enabled[1]=1; enabled[2]=3; break;
+                }
+              break;
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/objects/RubikObjectList.java b/src/main/java/org/distorted/objects/RubikObjectList.java
index d7af95ab..bf950764 100644
--- a/src/main/java/org/distorted/objects/RubikObjectList.java
+++ b/src/main/java/org/distorted/objects/RubikObjectList.java
@@ -74,6 +74,15 @@ public enum RubikObjectList
          new RubikMovementSkewb(),
          2
        ),
+
+  HELI (
+         new int[][] {
+                       {4 , 18, R.raw.skewb, R.drawable.ui_small_skewb, R.drawable.ui_medium_skewb, R.drawable.ui_big_skewb, R.drawable.ui_huge_skewb} ,
+                     },
+         RubikHelicopter.class,
+         new RubikMovementHelicopter(),
+         2
+       ),
   ;
 
   public static final int NUM_OBJECTS = values().length;
@@ -446,10 +455,11 @@ public enum RubikObjectList
 
     switch(ordinal())
       {
-      case 0: return new RubikCube    (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 1: return new RubikPyraminx(size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 2: return new RubikDino    (size, quat, texture, mesh, effects, moves, res, scrWidth);
-      case 3: return new RubikSkewb   (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 0: return new RubikCube      (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 1: return new RubikPyraminx  (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 2: return new RubikDino      (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 3: return new RubikSkewb     (size, quat, texture, mesh, effects, moves, res, scrWidth);
+      case 4: return new RubikHelicopter(size, quat, texture, mesh, effects, moves, res, scrWidth);
       }
 
     return null;
diff --git a/src/main/java/org/distorted/objects/RubikPyraminx.java b/src/main/java/org/distorted/objects/RubikPyraminx.java
index 6413cd23..c395741e 100644
--- a/src/main/java/org/distorted/objects/RubikPyraminx.java
+++ b/src/main/java/org/distorted/objects/RubikPyraminx.java
@@ -622,7 +622,7 @@ public class RubikPyraminx extends RubikObject
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// TODO  (only needed for solvers - there are no Pyraminx solvers ATM)
+// only needed for solvers - there are no Pyraminx solvers ATM)
 
   public String retObjectString()
     {
diff --git a/src/main/java/org/distorted/objects/RubikSkewb.java b/src/main/java/org/distorted/objects/RubikSkewb.java
index 26771b20..ab8d052b 100644
--- a/src/main/java/org/distorted/objects/RubikSkewb.java
+++ b/src/main/java/org/distorted/objects/RubikSkewb.java
@@ -685,7 +685,7 @@ public class RubikSkewb extends RubikObject
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// TODO  (only needed for solvers - there are no Skewb solvers ATM)
+// only needed for solvers - there are no Skewb solvers ATM)
 
   public String retObjectString()
     {
diff --git a/src/main/res/raw/compute_quats.c b/src/main/res/raw/compute_quats.c
index bfc004c3..0778b470 100644
--- a/src/main/res/raw/compute_quats.c
+++ b/src/main/res/raw/compute_quats.c
@@ -9,21 +9,23 @@
 #define PI  3.14159265358f
 #define NUM_QUATS  100
 
-#ifdef PYRAMIX 
+#ifdef PYRA 
 #define NUM_AXIS    4
 #define BASIC_ANGLE 3
 
-float axis[NUM_AXIS][3] ={ {         0,       1,       0 } ,
-                           { SQ2*SQ3/3, -1.0f/3,  -SQ2/3 } ,
-                           {-SQ2*SQ3/3, -1.0f/3,  -SQ2/3 } ,
-                           {         0, -1.0f/3, 2*SQ2/3 } };
+float axis[NUM_AXIS][3] = { {         0,       1,       0 } ,
+                            { SQ2*SQ3/3, -1.0f/3,  -SQ2/3 } ,
+                            {-SQ2*SQ3/3, -1.0f/3,  -SQ2/3 } ,
+                            {         0, -1.0f/3, 2*SQ2/3 } };
 #endif
 
 #ifdef CUBE
 #define NUM_AXIS    3
 #define BASIC_ANGLE 4
 
-float axis[NUM_AXIS][3] = { { 1,0,0 }, {0,1,0}, {0,0,1} };
+float axis[NUM_AXIS][3] = { { 1,0,0 } , 
+                            { 0,1,0 } , 
+                            { 0,0,1 } };
 #endif
 
 #ifdef DINO
@@ -36,8 +38,19 @@ float axis[NUM_AXIS][3] = { {+SQ3/3,+SQ3/3,+SQ3/3} ,
                             {+SQ3/3,-SQ3/3,-SQ3/3} };
 #endif
 
-float* quats;
-float* table;
+#ifdef HELI
+#define NUM_AXIS 6
+#define BASIC_ANGLE 2
+
+float axis[NUM_AXIS][3] = { {     0, +SQ2/2, -SQ2/2} ,
+                            {     0, -SQ2/2, -SQ2/2} ,
+                            {+SQ2/2,      0, -SQ2/2} ,
+                            {-SQ2/2,      0, -SQ2/2} ,
+                            {+SQ2/2, -SQ2/2,      0} ,
+                            {-SQ2/2, -SQ2/2,      0} };
+
+#endif
+
 int inserted=0;
 
 ///////////////////////////////////////////////////////////////////
@@ -117,29 +130,31 @@ void insert(float* quat, float* to)
 int main(int argc, char** argv)
   {
   float tmp[4];
-  int num = 1+NUM_AXIS*(BASIC_ANGLE-1);
-
-  quats = (float*) malloc(4*sizeof(float)*num      );
-  table = (float*) malloc(4*sizeof(float)*NUM_QUATS);
+  float table[4*NUM_QUATS];
+  int num;
 
   tmp[0] = 0.0f; tmp[1] = 0.0f; tmp[2] = 0.0f; tmp[3] = 1.0f;
-  insert(tmp,quats);
+  insert(tmp,table);
 
   for(int angle=1; angle<BASIC_ANGLE; angle++)
     for( int ax=0; ax<NUM_AXIS; ax++)
       {
       create_quat(axis[ax], 2*PI*angle/BASIC_ANGLE, tmp);
-      insert(tmp,quats); 
+      insert(tmp,table); 
       }
 
-  inserted=0;
-
-  for(int i=0; i<num; i++)
-    for(int j=0; j<num; j++)
-      {
-      multiply_quats( quats+4*i, quats+4*j, tmp);
-      insert(tmp,table);
-      }
+  do
+    {
+    num = inserted;
+
+    for(int i=0; i<num; i++)
+      for(int j=0; j<num; j++)
+        {
+        multiply_quats( table+4*i, table+4*j, tmp);
+        insert(tmp,table);
+        }
+    }
+  while( num < inserted );
 
   printf("inserted: %d\n", inserted);
 
