Major improvement to Carousel dragging.

This change adds new drag choices to Carousel:
DRAG_MODEL_PLANE, DRAG_MODEL_CYLINDER_INSIDE and DRAG_MODEL_CYLINDER_OUTSIDE.
The old drag model is still available as DRAG_MODEL_SCREEN_DELTA.

Change-Id: I339c21ceaa493fb302c6b57adebfa2063a68d69a
diff --git a/carousel/java/com/android/ex/carousel/CarouselController.java b/carousel/java/com/android/ex/carousel/CarouselController.java
index fc53642..f6920f1 100644
--- a/carousel/java/com/android/ex/carousel/CarouselController.java
+++ b/carousel/java/com/android/ex/carousel/CarouselController.java
@@ -26,7 +26,6 @@
 import android.renderscript.Mesh;
 import android.renderscript.RenderScriptGL;
 import android.util.Log;
-import android.view.SurfaceHolder;
 
 /**
  * <p>
@@ -50,7 +49,6 @@
     private RenderScriptGL mRS;
     private static final String TAG = "CarouselController";
     private static final boolean DBG = false;
-    private boolean mTracking;
 
     // These shadow the state of the renderer in case the surface changes so the surface
     // can be restored to its previous state.
@@ -86,6 +84,7 @@
     private long mFadeInDuration = 250L;
     private Bitmap mDetailLoadingBitmap = Bitmap.createBitmap(
             new int[] {0}, 0, 1, 1, 1, Bitmap.Config.ARGB_4444);
+    private int mDragModel = CarouselRS.DRAG_MODEL_SCREEN_DELTA;
 
     public CarouselController() {
         boolean useDepthBuffer = true;
@@ -122,6 +121,7 @@
         setSwaySensitivity(mSwaySensitivity);
         setFrictionCoefficient(mFrictionCoefficient);
         setDragFactor(mDragFactor);
+        setDragModel(mDragModel);
         setLookAt(mEye, mAt, mUp);
         setRezInCardCount(mRezInCardCount);
         setFadeInDuration(mFadeInDuration);
@@ -494,6 +494,22 @@
         }
     }
 
+    /**
+     * Sets the current model for dragging. There are currently four drag models:
+     * {@link CarouselView#DRAG_MODEL_SCREEN_DELTA}
+     * {@link CarouselView#DRAG_MODEL_PLANE}
+     * {@link CarouselView#DRAG_MODEL_CYLINDER_INSIDE}
+     * {@link CarouselView#DRAG_MODEL_CYLINDER_OUTSIDE}
+     *
+     * @param model
+     */
+    public void setDragModel(int model) {
+        mDragModel  = model;
+        if (mRenderScript != null) {
+            mRenderScript.setDragModel(model);
+        }
+    }
+
     public void setCardRotation(float cardRotation) {
         mCardRotation = cardRotation;
         if (mRenderScript != null) {
diff --git a/carousel/java/com/android/ex/carousel/CarouselRS.java b/carousel/java/com/android/ex/carousel/CarouselRS.java
index dee79e5..c9c6578 100644
--- a/carousel/java/com/android/ex/carousel/CarouselRS.java
+++ b/carousel/java/com/android/ex/carousel/CarouselRS.java
@@ -48,6 +48,12 @@
     public static final int CMD_INVALIDATE_DETAIL_TEXTURE = 610;
     public static final int CMD_PING = 1000; // for debugging
 
+    // Drag models *** THIS LIST MUST MATCH THOSE IN carousel.rs ***
+    public static final int DRAG_MODEL_SCREEN_DELTA = 0;
+    public static final int DRAG_MODEL_PLANE = 1;
+    public static final int DRAG_MODEL_CYLINDER_INSIDE = 2;
+    public static final int DRAG_MODEL_CYLINDER_OUTSIDE = 3;
+
     private static final String TAG = "CarouselRS";
     private static final int DEFAULT_SLOT_COUNT = 10;
     private static final boolean MIPMAP = false;
@@ -242,7 +248,7 @@
     }
 
     public void setRadius(float radius) {
-        mScript.set_radius(radius);
+        mScript.invoke_setRadius(radius);
     }
 
     public void setCardRotation(float cardRotation) {
@@ -265,6 +271,10 @@
         mScript.set_dragFactor(dragFactor);
     }
 
+    public void setDragModel(int model) {
+        mScript.set_dragModel(model);
+    }
+
     private void initVertexProgram() {
         ProgramVertex.Builder pvb = new ProgramVertex.Builder(mRS, null, null);
         mVertexProgram = pvb.create();
diff --git a/carousel/java/com/android/ex/carousel/CarouselView.java b/carousel/java/com/android/ex/carousel/CarouselView.java
index 950c39c..e7e57d4 100644
--- a/carousel/java/com/android/ex/carousel/CarouselView.java
+++ b/carousel/java/com/android/ex/carousel/CarouselView.java
@@ -20,17 +20,12 @@
 import com.android.ex.carousel.CarouselRS.CarouselCallback;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.PixelFormat;
-import android.graphics.Bitmap.Config;
-import android.renderscript.FileA3D;
 import android.renderscript.Float4;
 import android.renderscript.Mesh;
 import android.renderscript.RSSurfaceView;
 import android.renderscript.RenderScriptGL;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.SurfaceHolder;
 
@@ -45,7 +40,6 @@
 public abstract class CarouselView extends RSSurfaceView {
     private static final boolean USE_DEPTH_BUFFER = true;
     private static final String TAG = "CarouselView";
-    private static final boolean DBG = false;
     private CarouselRS mRenderScript;
     private RenderScriptGL mRS;
     private Context mContext;
@@ -53,6 +47,16 @@
 
     CarouselController mController;
 
+    // Drag relative to x coordinate of motion on screen
+    public static final int DRAG_MODEL_SCREEN_DELTA = CarouselRS.DRAG_MODEL_SCREEN_DELTA;
+    // Drag relative to projected point on plane of carousel
+    public static final int DRAG_MODEL_PLANE = CarouselRS.DRAG_MODEL_PLANE;
+    // Drag relative to projected point on inside (far point) of cylinder centered around carousel
+    public static final int DRAG_MODEL_CYLINDER_INSIDE = CarouselRS.DRAG_MODEL_CYLINDER_INSIDE;
+    // Drag relative to projected point on outside (near point) of cylinder centered around carousel
+    public static final int DRAG_MODEL_CYLINDER_OUTSIDE = CarouselRS.DRAG_MODEL_CYLINDER_OUTSIDE;
+
+
     // Note: remember to update carousel.rs when changing the values below
     public static class DetailAlignment {
         /** Detail is centered vertically with respect to the card **/
@@ -450,6 +454,10 @@
         mController.setDragFactor(dragFactor);
     }
 
+    public void setDragModel(int model) {
+        mController.setDragModel(model);
+    }
+
     public void setLookAt(float[] eye, float[] at, float[] up) {
         mController.setLookAt(eye, at, up);
     }
diff --git a/carousel/java/com/android/ex/carousel/carousel.rs b/carousel/java/com/android/ex/carousel/carousel.rs
index 2bcf946..62d113c 100644
--- a/carousel/java/com/android/ex/carousel/carousel.rs
+++ b/carousel/java/com/android/ex/carousel/carousel.rs
@@ -122,6 +122,12 @@
 static const int CMD_INVALIDATE_DETAIL_TEXTURE = 610;
 static const int CMD_PING = 1000;
 
+// Drag model *** THIS LIST MUST MATCH THOSE IN CarouselRS.java. ***
+static const int DRAG_MODEL_SCREEN_DELTA = 0; // Drag relative to x coordinate of motion vector
+static const int DRAG_MODEL_PLANE = 1; // Drag relative to projected point on plane of carousel
+static const int DRAG_MODEL_CYLINDER_INSIDE = 2; // Drag relative to point on inside of cylinder
+static const int DRAG_MODEL_CYLINDER_OUTSIDE = 3; // Drag relative to point on outside of cylinder
+
 // Constants
 static const int ANIMATION_SCALE_TIME = 200; // Time it takes to animate selected card, in ms
 static const float3 SELECTED_SCALE_FACTOR = { 0.2f, 0.2f, 0.2f }; // increase by this %
@@ -159,6 +165,7 @@
 float rezInCardCount; // this controls how rapidly distant card textures will be rez-ed in
 float detailFadeRate; // rate at which details fade as they move into the distance
 float4 backgroundColor;
+int dragModel = DRAG_MODEL_SCREEN_DELTA;
 rs_program_store programStoreAlphaZ;
 rs_program_store programStoreAlphaNoZ;
 rs_program_store programStoreNoAlphaZ;
@@ -179,7 +186,7 @@
 FragmentShaderConstants* shaderConstants;
 rs_sampler linearClamp;
 
-#pragma rs export_func(createCards, copyCards, lookAt)
+#pragma rs export_func(createCards, copyCards, lookAt, setRadius)
 #pragma rs export_func(doStart, doStop, doMotion, doLongPress, doSelection)
 #pragma rs export_func(setTexture, setGeometry, setDetailTexture, debugCamera)
 #pragma rs export_func(setCarouselRotationAngle)
@@ -207,6 +214,11 @@
        0.0f // plane constant (= -dot(P, N))
 };
 
+static Cylinder carouselCylinder = {
+        {0.0f, 0.0f, 0.0f }, // center
+        1.0f // radius - update with carousel radius.
+};
+
 // Because allocations can't have 0 dimensions, we have to track whether or not
 // cards are valid separately.
 // TODO: Remove this dependency once allocations can have a zero dimension.
@@ -238,6 +250,8 @@
 static bool __attribute__((overloadable))
         makeRayForPixelAt(Ray* ray, rs_matrix4x4* model, rs_matrix4x4* proj, float x, float y);
 static float deltaTimeInSeconds(int64_t current);
+static bool rayPlaneIntersect(Ray* ray, Plane* plane, float* tout);
+static bool rayCylinderIntersect(Ray* ray, Cylinder* cylinder, float* tout);
 
 void init() {
     // initializers currently have a problem when the variables are exported, so initialize
@@ -248,7 +262,7 @@
     visibleSlotCount = 1;
     visibleDetailCount = 3;
     bias = 0.0f;
-    radius = 1.0f;
+    radius = carouselCylinder.radius = 1.0f;
     cardRotation = 0.0f;
     cardsFaceTangent = false;
     updateCamera = true;
@@ -269,6 +283,11 @@
     cardCount = (cardAllocationValid && cardAlloc.p != 0) ? rsAllocationGetDimX(cardAlloc) : 0;
 }
 
+void setRadius(float rad)
+{
+    radius = carouselCylinder.radius = rad;
+}
+
 void createCards(int n)
 {
     if (debugTextureLoading) {
@@ -848,6 +867,7 @@
 // Behavior/Physics
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 static int64_t lastTime = 0L; // keep track of how much time has passed between frames
+static float lastAngle = 0.0f;
 static float2 lastPosition;
 static bool animating = false;
 static float velocityThreshold = 0.1f * M_PI / 180.0f;
@@ -858,9 +878,48 @@
 static const float G = 9.80f; // gravity constant, in m/s
 static const float springConstant = 0.0f;
 
+// Computes a hit angle from the center of the carousel to a point on either a plane
+// or on a cylinder. If neither is hit, returns false.
+static bool hitAngle(float x, float y, float *angle)
+{
+    Ray ray;
+    makeRayForPixelAt(&ray, &camera, x, y);
+    float t = FLT_MAX;
+    if (dragModel == DRAG_MODEL_PLANE && rayPlaneIntersect(&ray, &carouselPlane, &t)) {
+        const float3 point = (ray.position + t*ray.direction);
+        const float3 direction = point - carouselPlane.point;
+        *angle = atan2(direction.x, direction.z);
+        if (debugSelection) rsDebug("Plane Angle = ", degrees(*angle));
+        return true;
+    } else if ((dragModel == DRAG_MODEL_CYLINDER_INSIDE || dragModel == DRAG_MODEL_CYLINDER_OUTSIDE)
+            && rayCylinderIntersect(&ray, &carouselCylinder, &t)) {
+        const float3 point = (ray.position + t*ray.direction);
+        const float3 direction = point - carouselCylinder.center;
+        *angle = atan2(direction.x, direction.z);
+        if (debugSelection) rsDebug("Cylinder Angle = ", degrees(*angle));
+        return true;
+    }
+    return false;
+}
+
 static float dragFunction(float x, float y)
 {
-    return dragFactor * ((x - lastPosition.x) / rsgGetWidth()) * M_PI;
+    float result;
+    float angle;
+    if (hitAngle(x, y, &angle)) {
+        result = angle - lastAngle;
+        // Handle singularity where atan2 switches between +- PI
+        if (result < -M_PI) {
+            result += 2.0f * M_PI;
+        } else if (result > M_PI) {
+            result -= 2.0f * M_PI;
+        }
+        lastAngle = angle;
+    } else {
+        // If we didn't hit anything or drag model wasn't plane or cylinder, we use screen delta
+        result = dragFactor * ((x - lastPosition.x) / rsgGetWidth()) * M_PI;
+    }
+    return result;
 }
 
 static float deltaTimeInSeconds(int64_t current)
@@ -891,6 +950,7 @@
 void doStart(float x, float y)
 {
     touchPosition = lastPosition = (float2) { x, y };
+    lastAngle = hitAngle(x,y, &lastAngle) ? lastAngle : 0.0f;
     velocity = 0.0f;
     velocityTracker = 0.0f;
     velocityTrackerCount = 0;
@@ -1045,14 +1105,14 @@
 
     // Nearest point
     const float t1 = (-B - disc) / denom;
-    if (t1 > tmin && t1 < *tout) {
+    if (dragModel == DRAG_MODEL_CYLINDER_OUTSIDE && t1 > tmin && t1 < *tout) {
         *tout = t1;
         return true;
     }
 
     // Far point
     const float t2 = (-B + disc) / denom;
-    if (t2 > tmin && t2 < *tout) {
+    if (dragModel == DRAG_MODEL_CYLINDER_INSIDE && t2 > tmin && t2 < *tout) {
         *tout = t2;
         return true;
     }
diff --git a/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java b/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java
index dfc377a..2d78ae2 100644
--- a/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java
+++ b/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java
@@ -141,6 +141,7 @@
         mView.setRezInCardCount(3.0f);
         mView.setFadeInDuration(250);
         mView.setVisibleDetails(VISIBLE_DETAIL_COUNT);
+        mView.setDragModel(CarouselView.DRAG_MODEL_PLANE);
         if (INCREMENTAL_ADD) {
             mView.postDelayed(mAddCardRunnable, 2000);
         }