/* cubicspin.c,v 1.1.1.1 1995/02/27 07:38:38 explorer Exp */

/*
 * Copyright (C) 1990, 1991, Mark Podlipec, Craig E. Kolb
 * All rights reserved.
 *
 * This software may be freely copied, modified, and redistributed
 * provided that this copyright notice is preserved on all copies.
 *
 * You may not distribute this software, in whole or in part, as part of
 * any commercial product without the express consent of the authors.
 *
 * There is no warranty or other guarantee of fitness of this software
 * for any purpose.  It is provided solely "as is".
 *
 */

#include "geom.h"
#include "cubicspin.h"

#define EVALPOLY(_p,_Deg,_x,_y) \
 { int _i; \
   _y = (_p)[(_Deg)]; \
   for(_i=((_Deg)-1);_i>=0;_i--) _y=(_x)*_y + (_p)[_i]; \
 }

static Methods *iCubicSpinMethods = NULL;
static char cubicspinName[] = "cubicspin";

unsigned long CubicSpinTests, CubicSpinHits;


/*****************************************************************************
 * Create & return reference to a cubicspin.
 */
GeomRef
CubicSpinCreate(pos, norm, zlen, a3, a2, a1, a0)
     Vector *pos,*norm;
     Float zlen,a3,a2,a1,a0;
{
  CubicSpin *cubicspin;
  Vector tmpnrm;
  Float maxval,start,tmpval,tmppos,c[3];
  
  if (zlen < EPSILON) {
    RLerror(RL_WARN, "cubicspin length not correct.\n");
    return (GeomRef)NULL;
  }
  
  tmpnrm = *norm;
  if (VecNormalize(&tmpnrm) == 0.) {
    RLerror(RL_WARN, "Degenerate cubicspin normal.\n");
    return (GeomRef)NULL;
  }
  
  cubicspin = (CubicSpin *)Malloc(sizeof(CubicSpin));
  
  cubicspin->sgnflag = 0;
  cubicspin->zlen = zlen;
  cubicspin->a[3] = a3;
  cubicspin->a[2] = a2;
  cubicspin->a[1] = a1;
  cubicspin->a[0] = a0;
  cubicspin->g[6] = -(a3 * a3);
  cubicspin->g[5] = -(2.0 * a3 * a2); 
  cubicspin->g[4] = -(2.0 * a3 * a1) - (a2 * a2);
  cubicspin->g[3] = - 2.0 * (a3 * a0 + a2 * a1);
  cubicspin->g[2] = -(2.0 * a2 * a0) - (a1 * a1);
  cubicspin->g[1] = -(2.0 * a1 * a0);
  cubicspin->g[0] = -(a0 * a0);
  
  /*
   * Calculate derivative of cubic equation to be rotated.
   * This is to be used to calculate the bounds of the cubic spin.
   */
  c[0] = a1;
  c[1] = 2.0 * a2;
  c[2] = 3.0 * a3;
  
  
  /*
   * Evaluate the cubic equation at the end points and at each max/min in
   * order to determine the maximum value over the range [0.0,zlen]. This
   * will be used to determine the bounds of the cubicspin. 
   * NOTE: there is a most two min/max's.
   */
  /*
   * Set maxval to value of cubic at the first end point, 0.0.
   */
  EVALPOLY(cubicspin->a,3,(Float)(0.0),maxval);
  if (maxval<0.0) { 
    maxval = -maxval; 
    cubicspin->sgnflag |= CUB_NEG_FLAG; 
  }
  else cubicspin->sgnflag |= CUB_POS_FLAG;
  
  /*
   * evaluate cubic at zlen and update maxval if larger.
   */
  EVALPOLY(cubicspin->a,3,zlen,tmpval);
  if (tmpval<0.0) { 
    tmpval = -tmpval; 
    cubicspin->sgnflag |= CUB_NEG_FLAG; 
  }
  else
    cubicspin->sgnflag |= CUB_POS_FLAG;
  if (tmpval>maxval) maxval=tmpval;
  
  /* 
   * Check for any min/max's in the range [0.0,zlen]
   */
  if (FindRoot(c,2,(Float)(0.0),zlen,&tmppos,-1)) {
    /*
     * evaluate cubic at first min/max and update maxval 
     * if larger.
     */
    EVALPOLY(cubicspin->a,3,tmppos,tmpval);
    if (tmpval<0.0) { 
      tmpval = -tmpval; 
      cubicspin->sgnflag |= CUB_NEG_FLAG; 
    }
    else
      cubicspin->sgnflag |= CUB_POS_FLAG;
    if (tmpval>maxval) maxval=tmpval;
    
    /*
     * Check for another min/max between 1st one and endpoint.
     */
    start = tmppos;
    if (FindRoot(c,2,start,zlen,&tmppos,-1)) {
      /*
       * evaluate cubic at first min/max and update 
       * maxval if larger.
       */
      EVALPOLY(cubicspin->a,3,tmppos,tmpval);
      if (tmpval<0.0) { 
	tmpval = -tmpval; 
	cubicspin->sgnflag |= CUB_NEG_FLAG; 
      }
      else
	cubicspin->sgnflag |= CUB_POS_FLAG;
      if (tmpval>maxval) maxval=tmpval;
    }
  }
  cubicspin->maxwidth = maxval + 5*EPSILON;
  
  CoordSysTransform(pos, &tmpnrm, 1., 1., &cubicspin->trans);
  
  return (GeomRef)cubicspin;
}




/*****************************************************************************
 * Ray/CubicSpin intersection test.
 */
int
CubicSpinIntersect(ref, inray, hl, mindist, maxdist)
     GeomRef ref;
     Ray *inray;
     HitList *hl;  /* unused here */
     Float mindist, *maxdist;
{
  CubicSpin *cubicspin = (CubicSpin *)ref;
  Vector pos,ray;
  Float dist,nmax,nmin;
  Float root,*g;
  Float x0,x1,y0,y1,z0,z1;
  Float raymin,raymax;
  Float c[7];
  int i;
  
  CubicSpinTests++;
  
  
  /* Transform ray into cubicspin space */
  {
    Float bounds[2][3];
    Ray tmpray;
    
    tmpray = *inray;
    /* distance factor will always be a 1.0 */
    nmin = RayTransform(&tmpray, &cubicspin->trans.itrans);
    
    ray = tmpray.dir;
    pos = tmpray.pos;
    nmin = mindist;
    nmax = *maxdist;
    
    /* 
     * Find point(s) of Intersection between ray and bounding volume.
     * Use these points as bounding values to the FindRoot routine.
     */
    
    bounds[LOW][X]  = bounds[LOW][Y]  = -cubicspin->maxwidth; 
    bounds[HIGH][X] = bounds[HIGH][Y] =  cubicspin->maxwidth;
    bounds[HIGH][Z]  = cubicspin->zlen;
    bounds[LOW][Z] = 0.0;
    
    /*
     * Find 1st Intersection Point with CubicSpin bounding volume.
     */
    raymin = nmax;
    if (BoundsIntersect(&tmpray,bounds,mindist,&raymin)) {
      /* Move ray up to that point 
       */
      tmpray.pos.x += raymin * tmpray.dir.x;
      tmpray.pos.y += raymin * tmpray.dir.y;
      tmpray.pos.z += raymin * tmpray.dir.z;
      
      /* 
       * Check for a 2nd Intersction with Cubicspin bounding volume
       */
      raymax = nmax; 
      if (BoundsIntersect(&tmpray,bounds,EPSILON,&raymax)) {
	/* adjust ray to start from just outside boundary 
	 */
	pos.x = tmpray.pos.x;
	pos.y = tmpray.pos.y;
	pos.z = tmpray.pos.z;
	dist = raymin;
	raymin = 0.0;
      } else { /* must've been inside to start with */
	/* gotten from 1st bounds intersection 
	 */
	raymax = raymin;
	/* original minimum transformed to cubicspin space. 
	 */
	raymin = nmin;  
	dist = 0.0;
      }
    } else /* no intersection. */
      return FALSE;
  } /* end of transform and get min/max */
  
  x0 = pos.x;   y0 = pos.y;   z0 = pos.z;
  x1 = ray.x;   y1 = ray.y;   z1 = ray.z;
  
  /*
   * A cubic polynomial rotated about an axis is described by
   * a 6th degree polynomial.
   * Here the line equation is subbed in and all the coefficients
   * are calculated.
   */
  g = cubicspin->g;
  {
    register Float tmp,tmp2;
    
    tmp = g[6]; 
    c[6] = c[0] =        tmp;
    c[5] = c[1] =  6.0 * tmp;
    c[4] = c[2] = 15.0 * tmp;
    c[3] =        20.0 * tmp;
    for(i=0;i<6;i++) c[i] *= z0;
    
    tmp=g[5];    c[0]+=tmp;    c[5]+=tmp;
    tmp*=5.0;    c[1]+=tmp;    c[4]+=tmp;
    tmp*=2.0;    c[2]+=tmp;    c[3]+=tmp;
    for(i=0;i<5;i++) c[i] *= z0;
    
    tmp=g[4];    c[0]+=tmp;    c[4]+=tmp;
    tmp*=4.0;    c[1]+=tmp;    c[3]+=tmp;
    tmp*=1.5;    c[2]+=tmp;
    for(i=0;i<4;i++) c[i] *= z0;
    
    tmp=g[3];    c[0]+=tmp;    c[3]+=tmp;
    tmp*=3.0;    c[1]+=tmp;    c[2]+=tmp;
    for(i=0;i<3;i++) c[i] *= z0;
    
    tmp=g[2];    c[0]+=tmp;    c[2]+=tmp;
    tmp*=2.0;    c[1]+=tmp;
    c[0]*=z0; c[1]*=z0;
    
    tmp=g[1];    c[0]+=tmp;    c[1]+=tmp;
    c[0]*=z0;
    
    c[0] += g[0];
    
    tmp = tmp2 = z1;  c[1] *= tmp;  /* z1   */
    tmp2 *= tmp;      c[2] *= tmp2; /* z1^2 */
    tmp  *= tmp2;     c[3] *= tmp;  /* z1^3 */
    tmp  *= tmp2;     c[5] *= tmp;  /* z1^5 */
    tmp2 *= tmp2;     c[4] *= tmp2; /* z1^4 */
    tmp  *= z1;       c[6] *= tmp;  /* z1^6 */
  }
  
  c[0] += x0*x0 + y0*y0;
  c[1] += 2.0 * (x1*x0 + y1*y0);
  c[2] += x1*x1 + y1*y1;
  
  /*
   * Feed the coefficients into the FindRoot routine.
   * It should returns the smallest root found in the 
   * given interval( [raymin,raymax] )
   */
  if ( FindRoot(c,6,raymin,raymax,&root,-1) ) {
    
    /* Since we moved the ray's starting point to the bounding box
     * for precision reasons, we must add the distance between the
     * box and the original starting point of the ray.
     */
    root += dist;
    
    /* Make sure root is within the original interval
     */
    if (  (root > mindist) && (root < *maxdist)  )  {
      *maxdist = root;
      CubicSpinHits++;
      return TRUE;
    }
    else return FALSE;
  }
  return FALSE;
}


/***********************************************
 * Find the Normal of a CubicSpin at a given point
 */
int
CubicSpinNormal(ref, rawpos, nrm, gnrm)
     GeomRef ref;
     Vector *rawpos, *nrm, *gnrm;
{ 
  CubicSpin *cubicspin = (CubicSpin *)ref;
  Vector pos;
  register Float tmp,posz,*c;
  
  /* Transform intersection point to cubicspin space. 
   */
  pos = *rawpos;
  PointTransform(&pos, &cubicspin->trans.itrans);
  
  /*  
   * Calculate x,y normal components
   */
  
  tmp = sqrt(pos.x * pos.x + pos.y * pos.y);
  posz = pos.z;
  nrm->x = pos.x/tmp;
  nrm->y = pos.y/tmp;
  
  /*
   * Calculate z normal component
   */
  c = cubicspin->a;
  nrm->z = -(  (3.0*c[3]*posz + 2.0*c[2])*posz + c[1]  ); 
  /*
   * If the cubic has a negative region, the z component of the normal
   * must be inverted in those areas. sgnflag is used as an 
   * optimization so cubics that are always positive or always
   * negative in the interval [0.0,zlen] will not be penalized
   * by having to check for this.
   */
  if (cubicspin->sgnflag==CUB_NEG_FLAG)
    nrm->z = -nrm->z;
  else if (cubicspin->sgnflag!=CUB_POS_FLAG) {
    tmp = ((c[3]*posz + c[2])*posz + c[1])*posz + c[0];
    if (tmp<0.0) nrm->z = -nrm->z;
  }
  
  /* Transform normal back to world space. 
   */
  NormalTransform(nrm, &cubicspin->trans.itrans);
  *gnrm = *nrm;
  return FALSE;
}

/***********************************************
 * For now this similar to the cylinder UV mapping.
 * I'd like to eventually base uv->v on the actual
 * length of the cubic curve and not just on z.
 *
 * if the cubic is  f() = az^3 + bz^2 + cz + d
 *
 *                                     _______________
 * the equ. of length is  integral of V 1 + (df/dz)^2  dz
 *
 * where df/dz = 3az^2 + 2bz + c
 *
 * I looked through the books I have and I couldn't find
 * how to integrate this. I haven't had time to continue
 * the effort.
 *
 *
 */
void
CubicSpinUV(ref, pos, norm, uv, dpdu, dpdv)
     GeomRef ref;
     Vector *pos, *norm, *dpdu, *dpdv;
     Vec2d *uv;
{
  CubicSpin *cubicspin = (CubicSpin *)ref;
  Vector npos;
  Float val;
  
  npos = *pos;
  PointTransform(&npos, &cubicspin->trans.itrans);
  
  uv->v = npos.z / cubicspin->zlen;
  
  EVALPOLY(cubicspin->a, 3, npos.z, val);
  if ((val>(-EPSILON)) && (val<EPSILON))
    uv->u = 0.; 
  else {
    if (val < 0.) val = -val;
    
    val = npos.x/val;
    
    if (val > 1.)       uv->u = 0.;
    else if (val < -1.) uv->u = 0.5;
    else           uv->u = acos(val) / TWOPI;
    if (npos.y < 0.)    uv->u = 1. - uv->u;
  }
  
  if (dpdu) {
    dpdu->x = -npos.y;
    dpdu->y = npos.x;
    dpdu->z = 0.;
    VecTransform(dpdu, &cubicspin->trans.trans);
    (void)VecNormalize(dpdu);
  }
  if (dpdv) {
    VecCross(norm, dpdu, dpdv);
    VecTransform(dpdv, &cubicspin->trans.trans);
    (void)VecNormalize(dpdv);
  }
}

/*****************************************************************************
 * Calculate the extent of the CubicSpin
 */
void
CubicSpinBounds(ref, bounds)
     GeomRef ref;
     Float bounds[2][3];
{
  CubicSpin *cubicspin = (CubicSpin *)ref;
  bounds[LOW][X]  = bounds[LOW][Y]  = -cubicspin->maxwidth; 
  bounds[HIGH][X] = bounds[HIGH][Y] =  cubicspin->maxwidth;
  bounds[HIGH][Z]  = cubicspin->zlen;
  bounds[LOW][Z] = 0.0;
  
  /*
   * Transform bounding box to world space.
   */
  BoundsTransform(&cubicspin->trans.trans, bounds);
}

char *
CubicSpinName()
{
  return cubicspinName;
}

void
CubicSpinStats(tests, hits)
     unsigned long *tests, *hits;
{
  *tests = CubicSpinTests;
  *hits = CubicSpinHits;
}

Methods *
CubicSpinMethods()
{
  if (iCubicSpinMethods == (Methods *)NULL) {
    iCubicSpinMethods = MethodsCreate();
#if 0
    iCubicSpinMethods->create = (GeomCreateFunc *)CubicSpinCreate;
#endif
    iCubicSpinMethods->methods = CubicSpinMethods;
    iCubicSpinMethods->name = CubicSpinName;
    iCubicSpinMethods->intersect = CubicSpinIntersect;
    iCubicSpinMethods->normal = CubicSpinNormal;
    iCubicSpinMethods->uv = CubicSpinUV;
    iCubicSpinMethods->bounds = CubicSpinBounds;
    iCubicSpinMethods->stats = CubicSpinStats;
    iCubicSpinMethods->checkbounds = TRUE;
    iCubicSpinMethods->closed = FALSE;
  }
  return iCubicSpinMethods;
}
