Several major improvements to Carousel:

Improved selection criteria.  It now looks for the finger to move less
than a minimum distance (currently 50 pixels) before allowing that
card to be selected or the carousel to move.

Greatly simplified animation detection logic.

Started adding new motion model methods.

Change-Id: I8a7dc85dcfe547ff7bc024ba8be941bb787075ec
diff --git a/carousel/java/com/android/ex/carousel/CarouselViewHelper.java b/carousel/java/com/android/ex/carousel/CarouselViewHelper.java
index cda60b6..44685c1 100644
--- a/carousel/java/com/android/ex/carousel/CarouselViewHelper.java
+++ b/carousel/java/com/android/ex/carousel/CarouselViewHelper.java
@@ -257,11 +257,11 @@
     }
 
     public void onAnimationStarted() {
-
+        if (DBG) Log.v(TAG, "onAnimationStarted()");
     }
 
     public void onAnimationFinished(float startAngle) {
-
+        if (DBG) Log.v(TAG, "onAnimationFinished(" + startAngle + ")");
     }
 
     public void onResume() {
diff --git a/carousel/java/com/android/ex/carousel/carousel.rs b/carousel/java/com/android/ex/carousel/carousel.rs
index 193cba2..227921e 100644
--- a/carousel/java/com/android/ex/carousel/carousel.rs
+++ b/carousel/java/com/android/ex/carousel/carousel.rs
@@ -32,9 +32,8 @@
     int detailTextureState; // whether or not the detail for the card is loaded.
     int geometryState; // whether or not geometry is loaded
     int visible; // not bool because of packing bug?
-    // TODO: Change when int64_t is supported.  This will break after ~40 days of uptime.
-    unsigned int textureTimeStamp; // time when this texture was last updated, in seconds
-    unsigned int detailTextureTimeStamp; // time when this texture was last updated, in seconds
+    int64_t textureTimeStamp; // time when this texture was last updated, in seconds
+    int64_t detailTextureTimeStamp; // time when this texture was last updated, in seconds
 } Card_t;
 
 typedef struct Ray_s {
@@ -42,6 +41,17 @@
     float3 direction;
 } Ray;
 
+typedef struct Plane_s {
+    float3 point;
+    float3 normal;
+    float constant;
+} Plane;
+
+typedef struct Cylinder_s {
+    float3 center; // center of a y-axis-aligned infinite cylinder
+    float radius;
+} Cylinder;
+
 typedef struct PerspectiveCamera_s {
     float3 from;
     float3 at;
@@ -80,14 +90,16 @@
 // 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 %
+static const float OVERSCROLL_SLOTS = 1.0f; // amount of allowed overscroll (in slots)
 
 // Debug flags
 const bool debugCamera = false; // dumps ray/camera coordinate stuff
-const bool debugPicking = false; // renders picking area on top of geometry
+const bool debugSelection = false; // logs selection events
 const bool debugTextureLoading = false; // for debugging texture load/unload
 const bool debugGeometryLoading = false; // for debugging geometry load/unload
 const bool debugDetails = false; // for debugging detail texture geometry
 const bool debugRendering = false; // flashes display when the frame changes
+const bool debugRays = false; // shows visual depiction of hit tests, See renderWithRays().
 
 // Exported variables. These will be reflected to Java set_* variables.
 Card_t *cards; // array of cards to draw
@@ -113,6 +125,7 @@
 int fadeInDuration; // amount of time (in ms) for smoothly switching out textures
 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;
 rs_program_store programStore;
 rs_program_store programStoreOpaque;
 rs_program_store programStoreDetail;
@@ -134,19 +147,31 @@
 
 #pragma rs export_func(createCards, copyCards, lookAt)
 #pragma rs export_func(doStart, doStop, doMotion, doLongPress, doSelection)
-#pragma rs export_func(setTexture, setGeometry, setDetailTexture, debugCamera, debugPicking)
+#pragma rs export_func(setTexture, setGeometry, setDetailTexture, debugCamera)
 #pragma rs export_func(setCarouselRotationAngle)
 
 // Local variables
 static float bias; // rotation bias, in radians. Used for animation and dragging.
 static bool updateCamera;    // force a recompute of projection and lookat matrices
 static bool initialized;
-float4 backgroundColor;
 static const float FLT_MAX = 1.0e37;
-static int currentSelection = -1;
+static int animatedSelection = -1;
+static int currentFirstCard = -1;
 static int64_t touchTime = -1;  // time of first touch (see doStart())
 static float touchBias = 0.0f; // bias on first touch
+static float2 touchPosition; // position of first touch, as defined by last call to doStart(x,y)
 static float velocity = 0.0f;  // angular velocity in radians/s
+static bool overscroll = false; // whether we're in the overscroll animation
+static bool isDragging = false; // true while the user is dragging the carousel
+static float selectionRadius = 50.0f; // movement greater than this will result in no selection
+static bool enableSelection = false; // enabled until the user drags outside of selectionRadius
+
+// Default plane of the carousel. Used for angular motion estimation in view.
+static Plane carouselPlane = {
+       { 0.0f, 0.0f, 0.0f }, // point
+       { 0.0f, 1.0f, 0.0f }, // normal
+       0.0f // plane constant (= -dot(P, N))
+};
 
 // Because allocations can't have 0 dimensions, we have to track whether or not
 // cards are valid separately.
@@ -472,7 +497,7 @@
       rotation -= theta;
     }
     rsMatrixRotate(matrix, degrees(rotation), 0, 1, 0);
-    if (i == currentSelection) {
+    if (i == animatedSelection && enableSelection) {
         float3 scale = getAnimatedScaleForSelected();
         rsMatrixScale(matrix, scale.x, scale.y, scale.z);
     }
@@ -761,7 +786,6 @@
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 // Behavior/Physics
 ////////////////////////////////////////////////////////////////////////////////////////////////////
-static bool isDragging;
 static int64_t lastTime = 0L; // keep track of how much time has passed between frames
 static float2 lastPosition;
 static bool animating = false;
@@ -793,6 +817,10 @@
     return -1;
 }
 
+void sendAnimationStarted() {
+    rsSendToClient(CMD_ANIMATION_STARTED);
+}
+
 void sendAnimationFinished() {
     float data[1];
     data[0] = radiansToCarouselRotationAngle(bias);
@@ -801,98 +829,95 @@
 
 void doStart(float x, float y)
 {
-    lastPosition.x = x;
-    lastPosition.y = y;
+    touchPosition = lastPosition = (float2) { x, y };
     velocity = 0.0f;
-    if (animating) {
-        sendAnimationFinished();
-        animating = false;
-        currentSelection = -1;
-    } else {
-        currentSelection = doSelection(x, y);
-    }
     velocityTracker = 0.0f;
     velocityTrackerCount = 0;
     touchTime = rsUptimeMillis();
     touchBias = bias;
+    isDragging = true;
+    enableSelection = true;
+    animatedSelection = doSelection(x, y); // used to provide visual feedback on touch
 }
 
-
 void doStop(float x, float y)
 {
     int64_t currentTime = rsUptimeMillis();
     updateAllocationVars(cards);
-    if (currentSelection != -1 && (currentTime - touchTime) < ANIMATION_SCALE_TIME) {
-        // rsDebug("HIT!", currentSelection);
+
+    if (enableSelection) {
         int data[1];
-        data[0] = currentSelection;
-        rsSendToClientBlocking(CMD_CARD_SELECTED, data, sizeof(data));
+        int selection = doSelection(x, y);
+        if (selection != -1) {
+            if (debugSelection) rsDebug("Selected item on doStop():", selection);
+            data[0] = selection;
+            rsSendToClientBlocking(CMD_CARD_SELECTED, data, sizeof(data));
+        }
+        animating = false;
     } else {
+        // TODO: move velocity tracking to Java
         velocity = velocityTrackerCount > 0 ?
                     (velocityTracker / velocityTrackerCount) : 0.0f;  // avg velocity
         if (fabs(velocity) > velocityThreshold) {
             animating = true;
-            rsSendToClient(CMD_ANIMATION_STARTED);
         }
     }
-    currentSelection = -1;
+    enableSelection = false;
     lastTime = rsUptimeMillis();
+    isDragging = false;
 }
 
 void doLongPress()
 {
     int64_t currentTime = rsUptimeMillis();
     updateAllocationVars(cards);
-    if (currentSelection != -1) {
-        // rsDebug("HIT!", currentSelection);
+    // Selection happens for most recent position detected in doMotion()
+    int selection = doSelection(lastPosition.x, lastPosition.y);
+    if (selection != -1) {
+        if (debugSelection) rsDebug("doLongPress(), selection = ", selection);
         int data[1];
-        data[0] = currentSelection;
+        data[0] = selection;
         rsSendToClientBlocking(CMD_CARD_LONGPRESS, data, sizeof(data));
     }
-    currentSelection = -1;
     lastTime = rsUptimeMillis();
 }
 
 void doMotion(float x, float y)
 {
+    const float firstBias = wedgeAngle(0.0f);
+    const float lastBias = -max(0.0f, wedgeAngle(cardCount - visibleDetailCount));
     int64_t currentTime = rsUptimeMillis();
     float deltaOmega = dragFunction(x, y);
-    bias += deltaOmega;
-    lastPosition.x = x;
-    lastPosition.y = y;
+    if (!enableSelection) {
+        bias += deltaOmega;
+        bias = clamp(bias, lastBias - wedgeAngle(OVERSCROLL_SLOTS),
+                firstBias + wedgeAngle(OVERSCROLL_SLOTS));
+    }
+    const float2 delta = (float2) { x, y } - touchPosition;
+    float distance = sqrt(dot(delta, delta));
+    bool inside = (distance < selectionRadius);
+    enableSelection &= inside;
+    lastPosition = (float2) { x, y };
     float dt = deltaTimeInSeconds(currentTime);
     if (dt > 0.0f) {
         float v = deltaOmega / dt;
-        //if ((velocityTracker > 0.0f) == (v > 0.0f)) {
-            velocityTracker += v;
-            velocityTrackerCount++;
-        //} else {
-        //    velocityTracker = v;
-        //    velocityTrackerCount = 1;
-        //}
+        velocityTracker += v;
+        velocityTrackerCount++;
     }
     velocity = velocityTrackerCount > 0 ?
                 (velocityTracker / velocityTrackerCount) : 0.0f;  // avg velocity
-
-    // Drop current selection if user drags position +- a partial slot
-    if (currentSelection != -1) {
-        const float slotMargin = 0.5f * (2.0f * M_PI / slotCount);
-        if (fabs(touchBias - bias) > slotMargin) {
-            currentSelection = -1;
-        }
-    }
     lastTime = currentTime;
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 // Hit detection using ray casting.
 ////////////////////////////////////////////////////////////////////////////////////////////////////
+static const float EPSILON = 1.0e-6f;
+static const float tmin = 0.0f;
 
 static bool
-rayTriangleIntersect(Ray* ray, float3 p0, float3 p1, float3 p2, float *tout)
+rayTriangleIntersect(Ray* ray, float3 p0, float3 p1, float3 p2, float* tout)
 {
-    static const float tmin = 0.0f;
-
     float3 e1 = p1 - p0;
     float3 e2 = p2 - p0;
     float3 s1 = cross(ray->direction, e2);
@@ -917,16 +942,60 @@
     return true;
 }
 
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// Computes ray/plane intersection. Returns false if no intersection found.
+////////////////////////////////////////////////////////////////////////////////////////////////////
 static bool
-rayPlaneIntersect(Ray* ray, float3 point, float3 normal)
+rayPlaneIntersect(Ray* ray, Plane* plane, float* tout)
 {
-    return false; // TODO
+    float denom = dot(ray->direction, plane->normal);
+    if (fabs(denom) > EPSILON) {
+        float t = - (plane->constant + dot(ray->position, plane->normal)) / denom;
+        if (t > tmin && t < *tout) {
+            *tout = t;
+            return true;
+        }
+    }
+    return false;
 }
 
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// Computes ray/cylindr intersection. There are 0, 1 or 2 hits.
+// Returns true and sets *tout to the closest point or
+// returns false if no intersection found.
+////////////////////////////////////////////////////////////////////////////////////////////////////
 static bool
-rayCylinderIntersect(Ray* ray, float3 center, float radius)
+rayCylinderIntersect(Ray* ray, Cylinder* cylinder, float* tout)
 {
-    return false; // TODO
+    const float A = ray->direction.x * ray->direction.x + ray->direction.z * ray->direction.z;
+    if (A < EPSILON) return false; // ray misses
+
+    // Compute quadratic equation coefficients
+    const float B = 2.0f * (ray->direction.x * ray->position.x
+            + ray->direction.z * ray->position.z);
+    const float C = ray->position.x * ray->position.x
+            + ray->position.z * ray->position.z
+            - cylinder->radius * cylinder->radius;
+    float disc = B*B - 4*A*C;
+
+    if (disc < 0.0f) return false; // ray misses
+    disc = sqrt(disc);
+    const float denom = 2.0f * A;
+
+    // Nearest point
+    const float t1 = (-B - disc) / denom;
+    if (t1 > tmin && t1 < *tout) {
+        *tout = t1;
+        return true;
+    }
+
+    // Far point
+    const float t2 = (-B + disc) / denom;
+    if (t2 > tmin && t2 < *tout) {
+        *tout = t2;
+        return true;
+    }
+    return false;
 }
 
 // Creates a ray for an Android pixel coordinate given a camera, ray and coordinates.
@@ -1036,57 +1105,87 @@
 }
 
 // This method computes the position of all the cards by updating bias based on a
-// simple physics model.
-// If the cards are still in motion, returns true.
+// simple physics model.  If the cards are still in motion, returns true.
+static bool doPhysics(float dt)
+{
+    const float minStepTime = 1.0f / 300.0f; // ~5 steps per frame
+    const int N = (dt > minStepTime) ? (1 + round(dt / minStepTime)) : 1;
+    dt /= N;
+    for (int i = 0; i < N; i++) {
+        // Force friction - always opposes motion
+        const float Ff = -frictionCoeff * velocity;
+
+        // Restoring force to match cards with slots
+        const float theta = startAngle + bias;
+        const float dtheta = 2.0f * M_PI / slotCount;
+        const float position = theta / dtheta;
+        const float fraction = position - floor(position); // fractional position between slots
+        float x;
+        if (fraction > 0.5f) {
+            x = - (1.0f - fraction);
+        } else {
+            x = fraction;
+        }
+        const float Fr = - springConstant * x;
+
+        // compute velocity
+        const float momentum = mass * velocity + (Ff + Fr)*dt;
+        velocity = momentum / mass;
+        bias += velocity * dt;
+    }
+    return fabs(velocity) > velocityThreshold;
+}
+
+static float easeOut(float x)
+{
+    return x;
+}
+
+// Computes the next value for bias using the current animation (physics or overscroll)
 static bool updateNextPosition(int64_t currentTime)
 {
-    if (animating) {
-        float dt = deltaTimeInSeconds(currentTime);
-        if (dt <= 0.0f)
-            return animating;
-        const float minStepTime = 1.0f / 300.0f; // ~5 steps per frame
-        const int N = (dt > minStepTime) ? (1 + round(dt / minStepTime)) : 1;
-        dt /= N;
-        for (int i = 0; i < N; i++) {
-            // Force friction - always opposes motion
-            const float Ff = -frictionCoeff * velocity;
+    static const float biasMin = 1e-4f; // close enough if we're within this margin of result
 
-            // Restoring force to match cards with slots
-            const float theta = startAngle + bias;
-            const float dtheta = 2.0f * M_PI / slotCount;
-            const float position = theta / dtheta;
-            const float fraction = position - floor(position); // fractional position between slots
-            float x;
-            if (fraction > 0.5f) {
-                x = - (1.0f - fraction);
-            } else {
-                x = fraction;
-            }
-            const float Fr = - springConstant * x;
+    float dt = deltaTimeInSeconds(currentTime);
 
-            // compute velocity
-            const float momentum = mass * velocity + (Ff + Fr)*dt;
-            velocity = momentum / mass;
-            bias += velocity * dt;
-        }
-
-        animating = fabs(velocity) > velocityThreshold;
-        if (!animating) {
-            sendAnimationFinished();
-        }
+    if (dt <= 0.0f) {
+        if (debugRendering) rsDebug("Time delta was <= 0", dt);
+        return true;
     }
-    lastTime = currentTime;
 
     const float firstBias = wedgeAngle(0.0f);
     const float lastBias = -max(0.0f, wedgeAngle(cardCount - visibleDetailCount));
-
-    if (bias > firstBias) {
-        bias = firstBias;
-    } else if (bias < lastBias) {
-        bias = lastBias;
+    bool stillAnimating = false;
+    if (overscroll) {
+        if (bias > firstBias) {
+            bias -= 4.0f * dt * easeOut((bias - firstBias) * 2.0f);
+            if (fabs(bias - firstBias) < biasMin) {
+                bias = firstBias;
+            } else {
+                stillAnimating = true;
+            }
+        } else if (bias < lastBias) {
+            bias += 4.0f * dt * easeOut((lastBias - bias) * 2.0f);
+            if (fabs(bias - lastBias) < biasMin) {
+                bias = lastBias;
+            } else {
+                stillAnimating = true;
+            }
+        } else {
+            overscroll = false;
+        }
+    } else {
+        stillAnimating = doPhysics(dt);
+        overscroll = bias > firstBias || bias < lastBias;
     }
-
-    return animating;
+    float newbias = clamp(bias, lastBias - wedgeAngle(OVERSCROLL_SLOTS),
+            firstBias + wedgeAngle(OVERSCROLL_SLOTS));
+    if (newbias != bias) { // we clamped
+        velocity = 0.0f;
+        overscroll = true;
+    }
+    bias = newbias;
+    return stillAnimating;
 }
 
 // Cull cards based on visibility and visibleSlotCount.
@@ -1133,7 +1232,7 @@
     for (int i = cardCount-1; i >= 0; --i) {
         int data[1];
         if (cards[i].visible) {
-            if (debugTextureLoading) rsDebug("*** Texture stamp: ", cards[i].textureTimeStamp);
+            if (debugTextureLoading) rsDebug("*** Texture stamp: ", (int)cards[i].textureTimeStamp);
 
             // request texture from client if not loaded
             if (cards[i].textureState == STATE_INVALID) {
@@ -1238,10 +1337,9 @@
     updateAllocationVars(cards);
 
     if (!initialized) {
-        if (debugTextureLoading){
+        if (debugTextureLoading) {
             rsDebug("*** initialized was false, updating all cards (cards = ", cards);
         }
-        const float2 zero = {0.0f, 0.0f};
         for (int i = 0; i < cardCount; i++) {
             initCard(cards + i);
         }
@@ -1254,8 +1352,13 @@
 
     updateCameraMatrix(rsgGetWidth(), rsgGetHeight());
 
-    const bool timeExpired = (currentTime - touchTime) > ANIMATION_SCALE_TIME;
-    bool stillAnimating = updateNextPosition(currentTime) || !timeExpired;
+    bool stillAnimating = (currentTime - touchTime) <= ANIMATION_SCALE_TIME;
+
+    if (!isDragging && animating) {
+        stillAnimating = updateNextPosition(currentTime);
+    }
+
+    lastTime = currentTime;
 
     cullCards();
 
@@ -1271,11 +1374,22 @@
     rsgBindProgramStore(programStoreDetail);
     stillAnimating |= drawDetails(currentTime);
 
-    if (debugPicking) {
+    if (stillAnimating != animating) {
+        if (stillAnimating) {
+            // we just started animating
+            sendAnimationStarted();
+        } else {
+            // we were animating but stopped animating just now
+            sendAnimationFinished();
+        }
+        animating = stillAnimating;
+    }
+
+    if (debugRays) {
         renderWithRays();
     }
 
     //rsSendToClient(CMD_PING);
 
-    return stillAnimating ? 1 : 0;
+    return animating ? 1 : 0;
 }
diff --git a/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java b/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java
index 3b6b81f..8c1cffd 100644
--- a/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java
+++ b/carousel/test/src/com/android/carouseltest/CarouselTestActivity.java
@@ -35,7 +35,7 @@
 public class CarouselTestActivity extends Activity {
     private static final String TAG = "CarouselTestActivity";
     private static final int CARD_SLOTS = 56;
-    private static final int TOTAL_CARDS = 1000;
+    private static final int TOTAL_CARDS = 100;
     private static final int TEXTURE_HEIGHT = 256;
     private static final int TEXTURE_WIDTH = 256;
     private static final int SLOTS_VISIBLE = 7;