/* pinion, Copyright (c) 2004 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

#include <X11/Intrinsic.h>

extern XtAppContext app;

#define PROGCLASS       "Pinion"
#define HACK_INIT       init_pinion
#define HACK_DRAW       draw_pinion
#define HACK_RESHAPE    reshape_pinion
#define HACK_HANDLE_EVENT pinion_handle_event
#define EVENT_MASK      PointerMotionMask
#define sws_opts        xlockmore_opts

#define DEF_SPIN_SPEED   "1.0"
#define DEF_SCROLL_SPEED "1.0"
#define DEF_GEAR_SIZE    "1.0"
#define DEF_MAX_RPM      "900"

#define DEFAULTS        "*delay:        15000              \n" \
                        "*showFPS:      False              \n" \
                        "*wireframe:    False              \n" \
                        "*titleFont:  -*-times-bold-r-normal-*-180-*\n" \
                        "*titleFont2: -*-times-bold-r-normal-*-120-*\n" \
                        "*titleFont3: -*-times-bold-r-normal-*-80-*\n"  \

#undef countof
#define countof(x) (sizeof((x))/sizeof((*x)))

#undef BELLRAND
#define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)

#include "xlockmore.h"
#include "normals.h"
#include "gltrackball.h"
#include "glxfonts.h"
#include <ctype.h>

#ifdef USE_GL /* whole file */

#include <GL/glu.h>

typedef struct {
  unsigned long id;          /* unique name */
  double x, y, z;            /* position */
  double r;                  /* radius of the gear, at middle of teeth */
  double th;                 /* rotation (degrees) */

  GLint nteeth;              /* how many teeth */
  double tooth_w, tooth_h;   /* size of teeth */

  double inner_r;            /* radius of the (larger) inside hole */
  double inner_r2;           /* radius of the (smaller) inside hole, if any */
  double inner_r3;           /* yet another */

  double thickness;          /* height of the edge */
  double thickness2;         /* height of the (smaller) inside disc if any */
  double thickness3;         /* yet another */
  int spokes;                /* how many spokes inside, if any */
  int nubs;                  /* how many little nubbly bits, if any */
  double spoke_thickness;    /* spoke versus hole */
  GLfloat wobble;            /* factory defect! */
  int motion_blur_p;	     /* whether it's spinning too fast to draw */
  int polygons;              /* how many polys in this gear */

  double ratio;              /* gearing ratio with previous gears */
  double rpm;                /* approximate revolutions per minute */

  Bool base_p;               /* whether this gear begins a new train */
  int coax_p;                /* whether this is one of a pair of bound gears.
                                1 for first, 2 for second. */
  double coax_thickness;     /* thickness of the other gear in the pair */
  enum { SMALL, MEDIUM, LARGE } size;	/* Controls complexity of mesh. */
  GLfloat color[4];
  GLfloat color2[4];

  GLuint dlist;
} gear;


typedef struct {
  GLXContext *glx_context;
  GLfloat vp_left, vp_right, vp_top, vp_bottom;    /* default visible area */
  GLfloat vp_width, vp_height;
  GLfloat render_left, render_right;  /* area in which gears are displayed */
  GLfloat layout_left, layout_right;  /* layout region, on the right side  */

  int ngears;
  int gears_size;
  gear **gears;

  trackball_state *trackball;
  Bool button_down_p;
  unsigned long mouse_gear_id;

  XFontStruct *xfont1, *xfont2, *xfont3;
  GLuint font1_dlist, font2_dlist, font3_dlist;
  GLuint title_list;

} pinion_configuration;


static pinion_configuration *pps = NULL;
static GLfloat spin_speed, scroll_speed, max_rpm, gear_size;
static GLfloat plane_displacement = 0.1;  /* distance between coaxial gears */

static Bool verbose_p = False;            /* print progress on stderr */
static Bool debug_placement_p = False;    /* extreme verbosity on stderr */
static Bool debug_p = False;              /* render as flat schematic */
static Bool debug_one_gear_p = False;     /* draw one big stationary gear */
static Bool wire_all_p = False;           /* in wireframe, do not abbreviate */

static int debug_size_failures;           /* for debugging messages */
static int debug_position_failures;
static unsigned long current_length;      /* gear count in current train */
static unsigned long current_blur_length; /* how long have we been blurring? */


static XrmOptionDescRec opts[] = {
  { "-spin",   ".spinSpeed",   XrmoptionSepArg, 0 },
  { "-scroll", ".scrollSpeed", XrmoptionSepArg, 0 },
  { "-size",   ".gearSize",    XrmoptionSepArg, 0 },
  { "-max-rpm",".maxRPM",      XrmoptionSepArg, 0 },
  { "-debug",  ".debug",       XrmoptionNoArg, "True" },
  { "-verbose",".verbose",     XrmoptionNoArg, "True" },
};

static argtype vars[] = {
  {&spin_speed,   "spinSpeed",   "SpinSpeed",   DEF_SPIN_SPEED,   t_Float},
  {&scroll_speed, "scrollSpeed", "ScrollSpeed", DEF_SCROLL_SPEED, t_Float},
  {&gear_size,    "gearSize",    "GearSize",    DEF_GEAR_SIZE,    t_Float},
  {&max_rpm,      "maxRPM",      "MaxRPM",      DEF_MAX_RPM,      t_Float},
  {&debug_p,      "debug",       "Debug",       "False",          t_Bool},
  {&verbose_p,    "verbose",     "Verbose",     "False",          t_Bool},
};

ModeSpecOpt sws_opts = {countof(opts), opts, countof(vars), vars, NULL};


/* Font stuff
 */

static void
load_fonts (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  load_font (mi->dpy, "titleFont",  &pp->xfont1, &pp->font1_dlist);
  load_font (mi->dpy, "titleFont2", &pp->xfont2, &pp->font2_dlist);
  load_font (mi->dpy, "titleFont3", &pp->xfont3, &pp->font3_dlist);
}



static void rpm_string (double rpm, char *s);

static void
new_label (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  char label[1024];
  int i;
  gear *g = 0;

  if (pp->mouse_gear_id)
    for (i = 0; i < pp->ngears; i++)
      if (pp->gears[i]->id == pp->mouse_gear_id)
        {
          g = pp->gears[i];
          break;
        }

  if (!g)
    *label = 0;
  else
    {
      sprintf (label, "%d teeth\n", g->nteeth);
      rpm_string (g->rpm, label + strlen(label));
      if (debug_p)
        sprintf (label + strlen (label), "\nPolys:  %d\nModel:  %s  (%.2f)\n",
                 g->polygons,
                 (g->size == SMALL ? "small" : g->size == MEDIUM ? "medium"
                  : "large"),
                 g->tooth_h * MI_HEIGHT(mi));
    }

  glNewList (pp->title_list, GL_COMPILE);
  if (*label)
    {
      XFontStruct *f;
      GLuint fl;
      if (MI_WIDTH(mi) >= 500 && MI_HEIGHT(mi) >= 375)
        f = pp->xfont1, fl = pp->font1_dlist;                  /* big font */
      else if (MI_WIDTH(mi) >= 350 && MI_HEIGHT(mi) >= 260)
        f = pp->xfont2, fl = pp->font2_dlist;                  /* small font */
      else
        f = pp->xfont3, fl = pp->font3_dlist;                  /* tiny font */

      glColor3f (0.8, 0.8, 0);
      print_gl_string (mi->dpy, f, fl,
                       mi->xgwa.width, mi->xgwa.height,
                       10, mi->xgwa.height - 10,
                       label);
    }
  glEndList ();
}


/* Some utilities
 */


/* Find the gear in the scene that is farthest to the right or left.
 */
static gear *
farthest_gear (ModeInfo *mi, Bool left_p)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  int i;
  gear *rg = 0;
  double x = (left_p ? 999999 : -999999);
  for (i = 0; i < pp->ngears; i++)
    {
      gear *g = pp->gears[i];
      double gx = g->x + ((g->r + g->tooth_h) * (left_p ? -1 : 1));
      if (left_p ? (x > gx) : (x < gx))
        {
          rg = g;
          x = gx;
        }
    }
  return rg;
}


/* Compute the revolutions per minute of a gear.
 */
static void
compute_rpm (ModeInfo *mi, gear *g)
{
  double fps, rpf, rps;
  fps = (MI_PAUSE(mi) == 0 ? 999999 : 1000000.0 / MI_PAUSE(mi));

  if (fps > 150) fps = 150;  /* let's be reasonable... */
  if (fps < 10)  fps = 10;

  rpf    = (g->ratio * spin_speed) / 360.0;   /* rotations per frame */
  rps    = rpf * fps;                         /* rotations per second */
  g->rpm = rps * 60;
}

/* Prints the RPM into a string, doing fancy float formatting.
 */
static void
rpm_string (double rpm, char *s)
{
  char buf[30];
  int L;
  if (rpm >= 0.1)          sprintf (buf, "%.2f", rpm);
  else if (rpm >= 0.001)   sprintf (buf, "%.4f", rpm);
  else if (rpm >= 0.00001) sprintf (buf, "%.8f", rpm);
  else                     sprintf (buf, "%.16f",rpm);

  L = strlen(buf);
  while (buf[L-1] == '0') buf[--L] = 0;
  if (buf[L-1] == '.') buf[--L] = 0;
  strcpy (s, buf);
  strcat (s, " RPM");
}



/* Which of the gear's inside rings is the biggest? 
 */
static int
biggest_ring (gear *g, double *posP, double *sizeP, double *heightP)
{
  double r0 = (g->r - g->tooth_h/2);
  double r1 = g->inner_r;
  double r2 = g->inner_r2;
  double r3 = g->inner_r3;
  double w1 = (r1 ? r0 - r1 : r0);
  double w2 = (r2 ? r1 - r2 : 0);
  double w3 = (r3 ? r2 - r3 : 0);
  double h1 = g->thickness;
  double h2 = g->thickness2;
  double h3 = g->thickness3;

  if (g->spokes) w2 = 0;

  if (w1 > w2 && w1 > w3)
    {
      if (posP)    *posP = (r0+r1)/2;
      if (sizeP)   *sizeP = w1;
      if (heightP) *heightP = h1;
      return 0;
    }
  else if (w2 > w1 && w2 > w3)
    {
      if (posP)  *posP = (r1+r2)/2;
      if (sizeP) *sizeP = w2;
      if (heightP) *heightP = h2;
      return 1;
    }
  else
    {
      if (posP)  *posP = (r2+r3)/2;
      if (sizeP) *sizeP = w3;
      if (heightP) *heightP = h3;
      return 1;
    }
}


/* Layout and stuff.
 */


/* Create and return a new gear sized for placement next to or on top of
   the given parent gear (if any.)  Returns 0 if out of memory.
 */
static gear *
new_gear (ModeInfo *mi, gear *parent, Bool coaxial_p)
{
  gear *g = (gear *) calloc (1, sizeof (*g));
  int loop_count = 0;
  static unsigned long id = 0;

  if (!g) return 0;
  if (coaxial_p && !parent) abort();
  g->id = ++id;

  while (1)
    {
      loop_count++;
      if (loop_count > 1000)
        /* The only time we loop in here is when making a coaxial gear, and
           trying to pick a radius that is either significantly larger or
           smaller than its parent.  That shouldn't be hard, so something
           must be really wrong if we can't do that in only a few tries.
         */
        abort();

      /* Pick the size of the teeth.
       */
      if (parent && !coaxial_p) /* adjascent gears need matching teeth */
        {
          g->tooth_w = parent->tooth_w;
          g->tooth_h = parent->tooth_h;
          g->thickness  = parent->thickness;
          g->thickness2 = parent->thickness2;
          g->thickness3 = parent->thickness3;
        }
      else                 /* gears that begin trains get any size they want */
        {
          double scale = (1.0 + BELLRAND(4.0)) * gear_size;
          g->tooth_w = 0.007 * scale;
          g->tooth_h = 0.005 * scale;
          g->thickness  = g->tooth_h * (0.1 + BELLRAND(1.5));
          g->thickness2 = g->thickness / 4;
          g->thickness3 = g->thickness;
        }

      /* Pick the number of teeth, and thus, the radius.
       */
      {
        double c;

      AGAIN:
        g->nteeth = 3 + (random() % 97);    /* from 3 to 100 teeth */

        if (g->nteeth < 7 && (random() % 5) != 0)
          goto AGAIN;   /* Let's make very small tooth-counts more rare */

        c = g->nteeth * g->tooth_w * 2;     /* circumference = teeth + gaps */
        g->r = c / (M_PI * 2);              /* c = 2 pi r  */
      }


      /* Are we done now?
       */
      if (! coaxial_p) break;   /* yes */
      if (g->nteeth == parent->nteeth) continue; /* ugly */
      if (g->r  < parent->r * 0.6) break;  /* g much smaller than parent */
      if (parent->r < g->r  * 0.6) break;  /* g much larger than parent  */
    }

  /* Colorize
   */
  g->color[0] = 0.5 + frand(0.5);
  g->color[1] = 0.5 + frand(0.5);
  g->color[2] = 0.5 + frand(0.5);
  g->color[3] = 1.0;

  g->color2[0] = g->color[0] * 0.85;
  g->color2[1] = g->color[1] * 0.85;
  g->color2[2] = g->color[2] * 0.85;
  g->color2[3] = g->color[3];


  /* Decide on shape of gear interior:
     - just a ring with teeth;
     - that, plus a thinner in-set "plate" in the middle;
     - that, plus a thin raised "lip" on the inner plate;
     - or, a wide lip (really, a thicker third inner plate.)
   */
  if ((random() % 10) == 0)
    {
      /* inner_r can go all the way in; there's no inset disc. */
      g->inner_r = (g->r * 0.1) + frand((g->r - g->tooth_h/2) * 0.8);
      g->inner_r2 = 0;
      g->inner_r3 = 0;
    }
  else
    {
      /* inner_r doesn't go in very far; inner_r2 is an inset disc. */
      g->inner_r  = (g->r * 0.5)  + frand((g->r - g->tooth_h) * 0.4);
      g->inner_r2 = (g->r * 0.1) + frand(g->inner_r * 0.5);
      g->inner_r3 = 0;

      if (g->inner_r2 > (g->r * 0.2))
        {
          int nn = (random() % 10);
          if (nn <= 2)
            g->inner_r3 = (g->r * 0.1) + frand(g->inner_r2 * 0.2);
          else if (nn <= 7 && g->inner_r2 >= 0.1)
            g->inner_r3 = g->inner_r2 - 0.01;
        }
    }

  /* Coaxial gears need to have the same innermost hole size (for the axle.)
     Use whichever of the two is smaller.  (Modifies parent.)
   */
  if (coaxial_p)
    {
      double hole1 = (g->inner_r3 ? g->inner_r3 :
                      g->inner_r2 ? g->inner_r2 :
                      g->inner_r);
      double hole2 = (parent->inner_r3 ? parent->inner_r3 :
                      parent->inner_r2 ? parent->inner_r2 :
                      parent->inner_r);
      double hole = (hole1 < hole2 ? hole1 : hole2);
      if (hole <= 0) abort();

      if      (g->inner_r3) g->inner_r3 = hole;
      else if (g->inner_r2) g->inner_r2 = hole;
      else                  g->inner_r  = hole;

      if      (parent->inner_r3) parent->inner_r3 = hole;
      else if (parent->inner_r2) parent->inner_r2 = hole;
      else                       parent->inner_r  = hole;
    }

  /* If we have three discs, sometimes make the middle disc be spokes.
   */
  if (g->inner_r3 && ((random() % 5) == 0))
    {
      g->spokes = 2 + BELLRAND (5);
      g->spoke_thickness = 1 + frand(7.0);
      if (g->spokes == 2 && g->spoke_thickness < 2)
        g->spoke_thickness += 1;
    }

  /* Sometimes add little nubbly bits, if there is room.
   */
  if (g->nteeth > 5)
    {
      double size = 0;
      biggest_ring (g, 0, &size, 0);
      if (size > g->r * 0.2 && (random() % 5) == 0)
        {
          g->nubs = 1 + (random() % 16);
          if (g->nubs > 8) g->nubs = 1;
        }
    }

  if (g->inner_r3 > g->inner_r2) abort();
  if (g->inner_r2 > g->inner_r) abort();
  if (g->inner_r  > g->r) abort();

  /* Decide how complex the polygon model should be.
   */
  {
    double pix = g->tooth_h * MI_HEIGHT(mi); /* approx. tooth size in pixels */
    if (pix <= 2.5)      g->size = SMALL;
    else if (pix <= 3.5) g->size = MEDIUM;
    else                 g->size = LARGE;
  }

  g->base_p = !parent;

  return g;
}


/* Given a newly-created gear, place it next to its parent in the scene,
   with its teeth meshed and the proper velocity.  Returns False if it
   didn't work.  (Call this a bunch of times until either it works, or
   you decide it's probably not going to.)
 */
static Bool
place_gear (ModeInfo *mi, gear *g, gear *parent, Bool coaxial_p)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];

  /* If this gear takes up more than 1/3rd of the screen, it's no good.
   */
  if (((g->r + g->tooth_h) * (6 / gear_size)) >= pp->vp_width ||
      ((g->r + g->tooth_h) * (6 / gear_size)) >= pp->vp_height)
    {
      if (verbose_p && debug_placement_p && 0)
        fprintf (stderr,
                 "%s: placement: too big: %.2f @ %.2f vs %.2f x %.2f\n",
                 progname,
                 (g->r + g->tooth_h), gear_size,
                 pp->vp_width, pp->vp_height);
      debug_size_failures++;
      return False;
    }

  /* Compute this gear's velocity.
   */
  if (! parent)
    {
      g->ratio = 0.8 + BELLRAND(0.4);  /* 0.8-1.2 = 8-12rpm @ 60fps */
      g->th = frand (90) * ((random() & 1) ? 1.0 : -1.0);
    }
  else if (coaxial_p)
    {
      g->ratio = parent->ratio; /* bound gears have the same ratio */
      g->th = parent->th;
      g->rpm = parent->rpm;
      g->wobble = parent->wobble;
    }
  else
    {
      /* Gearing ratio is the ratio of the number of teeth to previous gear
         (which is also the ratio of the circumferences.)
       */
      g->ratio = (double) parent->nteeth / (double) g->nteeth;

      /* Set our initial rotation to match that of the previous gear,
         multiplied by the gearing ratio.  (This is finessed later,
         once we know the exact position of the gear relative to its
         parent.)
      */
      g->th = -(parent->th * g->ratio);

      if (g->nteeth & 1)    /* rotate 1/2 tooth-size if odd number of teeth */
        {
          double off = (180.0 / g->nteeth);
          if (g->th > 0)
            g->th += off;
          else
            g->th -= off;
        }

      /* ratios are cumulative for all gears in the train. */
      g->ratio *= parent->ratio;
    }


  /* Place the gear relative to the parent.
   */

  if (! parent)
    {
      gear *rg = farthest_gear (mi, False);
      double right = (rg ? rg->x + rg->r + rg->tooth_h : 0);
      if (right < pp->layout_left) /* place off screen */
        right = pp->layout_left;

      g->x = right + g->r + g->tooth_h + (0.01 / gear_size);
      g->y = 0;
      g->z = 0;

      if (debug_one_gear_p)
        g->x = 0;
    }
  else if (coaxial_p)
    {
      double off = plane_displacement;

      g->x = parent->x;
      g->y = parent->y;
      g->z = parent->z + (g->r > parent->r      /* small gear on top */
                          ? -off : off);

      if (parent->r > g->r)     /* mark which is top and which is bottom */
        {
          parent->coax_p = 1;
          g->coax_p      = 2;
          parent->wobble = 0;   /* looks bad when axle moves */
        }
      else
        {
          parent->coax_p = 2;
          g->coax_p      = 1;
          g->wobble      = 0;
        }

      g->coax_thickness      = parent->thickness;
      parent->coax_thickness = g->thickness;

      /* Don't let the train get too close to or far from the screen.
         If we're getting too close, give up on this gear.
         (But getting very far away is fine.)
       */
      if (g->z >=  off * 4 ||
          g->z <= -off * 4)
        {
          if (verbose_p && debug_placement_p)
            fprintf (stderr, "%s: placement: bad depth: %.2f\n",
                     progname, g->z);
          debug_position_failures++;
          return False;
        }
    }
  else                          /* position it somewhere next to the parent. */
    {
      double r_off = parent->r + g->r;
      int angle;

      if ((random() % 3) != 0)
        angle = (random() % 240) - 120;   /* mostly -120 to +120 degrees */
      else
        angle = (random() % 360) - 180;   /* sometimes -180 to +180 degrees */

      g->x = parent->x + (cos ((double) angle * (M_PI / 180)) * r_off);
      g->y = parent->y + (sin ((double) angle * (M_PI / 180)) * r_off);
      g->z = parent->z;

      /* If the angle we picked would have positioned this gear
         more than halfway off screen, that's no good. */
      if (g->y > pp->vp_top ||
          g->y < pp->vp_bottom)
        {
          if (verbose_p && debug_placement_p)
            fprintf (stderr, "%s: placement: out of bounds: %s\n",
                     progname, (g->y > pp->vp_top ? "top" : "bottom"));
          debug_position_failures++;
          return False;
        }

      /* avoid accidentally changing sign of "th" in the math below. */
      g->th += (g->th > 0 ? 360 : -360);

      /* Adjust the rotation of the gear so that its teeth line up with its
         parent, based on the position of the gear and the current rotation
         of the parent.
       */
      {
        double p_c = 2 * M_PI * parent->r;  /* circumference of parent */
        double g_c = 2 * M_PI * g->r;       /* circumference of g  */

        double p_t = p_c * (angle/360.0);   /* distance travelled along
                                               circumference of parent when
                                               moving "angle" degrees along
                                               parent. */
        double g_rat = p_t / g_c;           /* if travelling that distance
                                               along circumference of g,
                                               ratio of g's circumference
                                               travelled. */
        double g_th = 360.0 * g_rat;        /* that ratio in degrees */

        g->th += angle + g_th;
      }
    }

  if (debug_one_gear_p)
    {
      compute_rpm (mi, g);
      return True;
    }

  /* If the position we picked for this gear would cause it to already
     be visible on the screen, give up.  This can happen when the train
     is growing backwards, and we don't want to see gears flash into
     existence.
   */
  if (g->x - g->r - g->tooth_h < pp->render_right)
    {
      if (verbose_p && debug_placement_p)
        fprintf (stderr, "%s: placement: out of bounds: left\n", progname);
      debug_position_failures++;
      return False;
    }

  /* If the position we picked for this gear causes it to overlap
     with any earlier gear in the train, give up.
   */
  {
    int i;

    for (i = pp->ngears-1; i >= 0; i--)
      {
        gear *og = pp->gears[i];

        if (og == g) continue;
        if (og == parent) continue;
        if (g->z != og->z) continue;    /* Ignore unless on same layer */

        /* Collision detection without sqrt:
             d = sqrt(a^2 + b^2)   d^2 = a^2 + b^2
             d < r1 + r2           d^2 < (r1 + r2)^2
         */
        if (((g->x - og->x) * (g->x - og->x) +
             (g->y - og->y) * (g->y - og->y)) <
            ((g->r + g->tooth_h + og->r + og->tooth_h) *
             (g->r + g->tooth_h + og->r + og->tooth_h)))
          {
            if (verbose_p && debug_placement_p)
              fprintf (stderr, "%s: placement: collision with %lu\n",
                       progname, og->id);
            debug_position_failures++;
            return False;
          }
      }
  }

  compute_rpm (mi, g);


  /* Make deeper gears be darker.
   */
  {
    double depth = g->z / plane_displacement;
    double brightness = 1 + (depth / 6);
    double limit = 0.4;
    if (brightness < limit)   brightness = limit;
    if (brightness > 1/limit) brightness = 1/limit;
    g->color[0]  *= brightness;
    g->color[1]  *= brightness;
    g->color[2]  *= brightness;
    g->color2[0] *= brightness;
    g->color2[1] *= brightness;
    g->color2[2] *= brightness;
  }

  /* If a single frame of animation would cause the gear to rotate by
     more than 1/2 the size of a single tooth, then it won't look right:
     the gear will appear to be turning at some lower harmonic of its
     actual speed.
   */
  {
    double ratio = g->ratio * spin_speed;
    double blur_limit = 180.0 / g->nteeth;

    if (ratio > blur_limit)
      g->motion_blur_p = 1;

    if (!coaxial_p)
      {
        /* ride until the wheels fall off... */
        if (ratio > blur_limit * 0.7) g->wobble += (random() % 2);
        if (ratio > blur_limit * 0.9) g->wobble += (random() % 2);
        if (ratio > blur_limit * 1.1) g->wobble += (random() % 2);
        if (ratio > blur_limit * 1.3) g->wobble += (random() % 2);
        if (ratio > blur_limit * 1.5) g->wobble += (random() % 2);
        if (ratio > blur_limit * 1.7) g->wobble += (random() % 2);
      }
  }

  return True;
}

static void
free_gear (gear *g)
{
  if (g->dlist)
    glDeleteLists (g->dlist, 1);
  free (g);
}


/* Make a new gear, place it next to its parent in the scene,
   with its teeth meshed and the proper velocity.  Returns the gear;
   or 0 if it didn't work.  (Call this a bunch of times until either
   it works, or you decide it's probably not going to.)
 */
static gear *
place_new_gear (ModeInfo *mi, gear *parent, Bool coaxial_p)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  int loop_count = 0;
  gear *g = 0;

  while (1)
    {
      loop_count++;
      if (loop_count >= 100)
        {
          if (g)
            free_gear (g);
          g = 0;
          break;
        }

      g = new_gear (mi, parent, coaxial_p);
      if (!g) return 0;  /* out of memory? */

      if (place_gear (mi, g, parent, coaxial_p))
        break;
    }

  if (! g) return 0;

  /* We got a gear, and it is properly positioned.
     Insert it in the scene.
   */
  if (pp->ngears + 2 >= pp->gears_size)
    {
      pp->gears_size += 100;
      pp->gears = (gear **) realloc (pp->gears,
                                     pp->gears_size * sizeof (*pp->gears));
      if (! pp->gears)
        {
          fprintf (stderr, "%s: out of memory (%d gears)\n",
                   progname, pp->gears_size);
        }
    }

  pp->gears[pp->ngears++] = g;
  return g;
}


static void delete_gear (ModeInfo *mi, gear *g);

static void
push_gear (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  gear *g;
  gear *parent = (pp->ngears <= 0 ? 0 : pp->gears[pp->ngears-1]);

  Bool tried_coaxial_p = False;
  Bool coaxial_p = False;
  Bool last_ditch_coax_p = False;
  int loop_count = 0;

  debug_size_failures = 0;
  debug_position_failures = 0;

 AGAIN:
  loop_count++;
  if (loop_count > 100) abort();  /* we're doomed! */
  
  g = 0;

  /* If the gears are turning at LUDICROUS SPEED, unhook the train to
     reset things to a sane velocity.

     10,000 RPM at 30 FPS = 5.5 rotations per frame.
      1,000 RPM at 30 FPS = 0.5 rotations per frame.
        600 RPM at 30 FPS =  3 frames per rotation.
        200 RPM at 30 FPS =  9 frames per rotation.
        100 RPM at 30 FPS = 18 frames per rotation.
         50 RPM at 30 FPS = 36 frames per rotation.
         10 RPM at 30 FPS =  3 sec per rotation.
          1 RPM at 30 FPS = 30 sec per rotation.
         .5 RPM at 30 FPS =  1 min per rotation.
         .1 RPM at 30 FPS =  5 min per rotation.
   */
  if (parent && parent->rpm > max_rpm)
    {
      if (verbose_p)
        {
          char buf[100];
          rpm_string (parent->rpm, buf);
          fprintf (stderr, "%s: ludicrous speed!  %s\n\n", progname, buf);
        }
      parent = 0;
    }

  /* If the last N gears we've placed have all been motion-blurred, then
     it's a safe guess that we've wandered off into the woods and aren't
     coming back.  Bail on this train.
   */
  if (current_blur_length >= 10)
    {
      if (verbose_p)
        fprintf (stderr, "%s: it's a blurpocalypse!\n\n", progname);
      parent = 0;
    }



  /* Sometimes, try to make a coaxial gear.
   */
  if (parent && !parent->coax_p && (random() % 40) == 0)
    {
      tried_coaxial_p = True;
      coaxial_p = True;
      g = place_new_gear (mi, parent, coaxial_p);
    }

  /* Try to make a regular gear.
   */
  if (!g)
    {
      coaxial_p = False;
      g = place_new_gear (mi, parent, coaxial_p);
    }

  /* If we couldn't make a regular gear, then try to make a coxial gear
     (unless we already tried that.)
   */
  if (!g && !tried_coaxial_p && parent && !parent->coax_p)
    {
      tried_coaxial_p = True;
      coaxial_p = True;
      g = place_new_gear (mi, parent, coaxial_p);
      if (g)
        last_ditch_coax_p = True;
    }

  /* If we couldn't do that either, then the train has hit a dead end:
     start a new train.
   */
  if (!g)
    {
      coaxial_p = False;
      if (verbose_p)
        fprintf (stderr, "%s: dead end!\n\n", progname);
      parent = 0;
      g = place_new_gear (mi, parent, coaxial_p);
    }

  if (! g)
    {
      /* Unable to make/place any gears at all!
         This can happen if we've backed ourself into a corner very near
         the top-right or bottom-right corner of the growth zone.
         It's time to add a gear, but there's no room to add one!
         In that case, let's just wipe all the gears that are in the
         growth zone and try again.
       */
      int i;

      if (verbose_p && debug_placement_p)
        fprintf (stderr,
                 "%s: placement: resetting growth zone!  "
                 "failed: %d size, %d pos\n",
                 progname,
                 debug_size_failures, debug_position_failures);
      for (i = pp->ngears-1; i >= 0; i--)
        {
          gear *g = pp->gears[i];
          if (g->x - g->r - g->tooth_h < pp->render_left)
            delete_gear (mi, g);
        }
      goto AGAIN;
    }

  if (g->coax_p)
    {
      if (g->x != parent->x) abort();
      if (g->y != parent->y) abort();
      if (g->z == parent->z) abort();
      if (g->r == parent->r) abort();
      if (g->th != parent->th) abort();
      if (g->ratio != parent->ratio) abort();
      if (g->rpm != parent->rpm) abort();
    }

  if (verbose_p)
    {
      fprintf (stderr, "%s: %5lu ", progname, g->id);

      fputc ((g->motion_blur_p ? '*' : ' '), stderr);
      fputc (((g->coax_p && last_ditch_coax_p) ? '2' :
              g->coax_p ? '1' : ' '),
             stderr);

      fprintf (stderr, " %2d%%",
               (int) (g->r * 2 * 100 / pp->vp_height));
      fprintf (stderr, "  %2d teeth", g->nteeth);
      fprintf (stderr, " %3.0f rpm;", g->rpm);

      {
        char buf1[50], buf2[50], buf3[100];
        *buf1 = 0; *buf2 = 0; *buf3 = 0;
        if (debug_size_failures)
          sprintf (buf1, "%3d sz", debug_size_failures);
        if (debug_position_failures)
          sprintf (buf2, "%2d pos", debug_position_failures);
        if (*buf1 || *buf2)
          sprintf (buf3, " tries: %-7s%s", buf1, buf2);
        fprintf (stderr, "%-21s", buf3);
      }

      if (g->base_p) fprintf (stderr, " RESET %lu", current_length);
      fprintf (stderr, "\n");
    }

  if (g->base_p)
    current_length = 1;
  else
    current_length++;

  if (g->motion_blur_p)
    current_blur_length++;
  else
    current_blur_length = 0;
}



/* Remove the given gear from the scene and free it.
 */
static void
delete_gear (ModeInfo *mi, gear *g)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  int i;

  for (i = 0; i < pp->ngears; i++)      /* find this gear in the list */
    if (pp->gears[i] == g) break;
  if (pp->gears[i] != g) abort();

  for (; i < pp->ngears-1; i++)         /* pull later entries forward */
    pp->gears[i] = pp->gears[i+1];
  pp->gears[i] = 0;
  pp->ngears--;
  free_gear (g);
}


/* Update the position of each gear in the scene.
 */
static void
scroll_gears (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  int i;

  for (i = 0; i < pp->ngears; i++)
    pp->gears[i]->x -= (scroll_speed * 0.002);

  /* if the right edge of any gear is off screen to the left, delete it.
   */
  for (i = pp->ngears-1; i >= 0; i--)
    {
      gear *g = pp->gears[i];
      if (g->x + g->r + g->tooth_h < pp->render_left)
        delete_gear (mi, g);
    }

  /* if the right edge of the last-added gear is left of the right edge
     of the layout area, add another gear.
   */
  i = 0;
  while (1)
    {
      gear *g = (pp->ngears <= 0 ? 0 : pp->gears[pp->ngears-1]);
      if (!g || g->x + g->r + g->tooth_h < pp->layout_right)
        push_gear (mi);
      else
        break;
      i++;
      if (debug_one_gear_p) break;
    }

  /*
  if (i > 1 && verbose_p)
    fprintf (stderr, "%s: pushed %d gears\n", progname, i);
   */
}


/* Update the rotation of each gear in the scene.
 */
static void
spin_gears (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  int i;

  for (i = 0; i < pp->ngears; i++)
    {
      gear *g = pp->gears[i];
      double off = (g->ratio * spin_speed);

      if (g->th > 0)
        g->th += off;
      else
        g->th -= off;
    }
}


/* Run the animation fast (without displaying anything) until the first
   gear is just about to come on screen.  This is to avoid a big delay
   with a blank screen when -scroll is low.
 */
static void
ffwd (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  if (debug_one_gear_p) return;
  while (1)
    {
      gear *g = farthest_gear (mi, True);
      if (g && g->x - g->r - g->tooth_h/2 <= pp->vp_right * 0.88)
        break;
      scroll_gears (mi);
    }
}



/* Rendering the 3D objects into the scene.
 */


/* Draws an uncapped tube of the given radius extending from top to bottom,
   with faces pointing either in or out.
 */
static int
draw_ring (ModeInfo *mi, int segments,
           GLfloat r, GLfloat top, GLfloat bottom, Bool in_p)
{
  int i;
  int polys = 0;
  Bool wire_p = MI_IS_WIREFRAME(mi);
  GLfloat width = M_PI * 2 / segments;

  if (top != bottom)
    {
      glFrontFace (in_p ? GL_CCW : GL_CW);
      glBegin (wire_p ? GL_LINES : GL_QUAD_STRIP);
      for (i = 0; i < segments + (wire_p ? 0 : 1); i++)
        {
          GLfloat th = i * width;
          GLfloat cth = cos(th);
          GLfloat sth = sin(th);
          if (in_p)
            glNormal3f (-cth, -sth, 0);
          else
            glNormal3f (cth, sth, 0);
          glVertex3f (cth * r, sth * r, top);
          glVertex3f (cth * r, sth * r, bottom);
        }
      polys += segments;
      glEnd();
    }

  if (wire_p)
    {
      glBegin (GL_LINE_LOOP);
      for (i = 0; i < segments; i++)
        {
          GLfloat th = i * width;
          glVertex3f (cos(th) * r, sin(th) * r, top);
        }
      glEnd();
      glBegin (GL_LINE_LOOP);
      for (i = 0; i < segments; i++)
        {
          GLfloat th = i * width;
          glVertex3f (cos(th) * r, sin(th) * r, bottom);
        }
      glEnd();
    }

  return polys;
}


/* Draws a donut-shaped disc between the given radii,
   with faces pointing either up or down.
   The first radius may be 0, in which case, a filled disc is drawn.
 */
static int
draw_disc (ModeInfo *mi, int segments,
           GLfloat ra, GLfloat rb, GLfloat z, Bool up_p)
{
  int i;
  int polys = 0;
  Bool wire_p = MI_IS_WIREFRAME(mi);
  GLfloat width = M_PI * 2 / segments;

  if (ra <  0) abort();
  if (rb <= 0) abort();

  if (ra == 0)
    glFrontFace (up_p ? GL_CW : GL_CCW);
  else
    glFrontFace (up_p ? GL_CCW : GL_CW);

  if (ra == 0)
    glBegin (wire_p ? GL_LINES : GL_TRIANGLE_FAN);
  else
    glBegin (wire_p ? GL_LINES : GL_QUAD_STRIP);

  glNormal3f (0, 0, (up_p ? -1 : 1));

  if (ra == 0 && !wire_p)
    glVertex3f (0, 0, z);

  for (i = 0; i < segments + (wire_p ? 0 : 1); i++)
    {
      GLfloat th = i * width;
      GLfloat cth = cos(th);
      GLfloat sth = sin(th);
      if (wire_p || ra != 0)
        glVertex3f (cth * ra, sth * ra, z);
      glVertex3f (cth * rb, sth * rb, z);
    }
  polys += segments;
  glEnd();
  return polys;
}


/* Draws N thick radial lines between the given radii,
   with faces pointing either up or down.
 */
static int
draw_spokes (ModeInfo *mi, int n, GLfloat thickness, int segments,
             GLfloat ra, GLfloat rb, GLfloat z1, GLfloat z2)
{
  int i;
  int polys = 0;
  Bool wire_p = MI_IS_WIREFRAME(mi);
  GLfloat width;
  int segments2 = 0;
  int insegs, outsegs;
  int tick;
  int state;

  if (ra <= 0 || rb <= 0) abort();

  segments *= 3;

  while (segments2 < segments) /* need a multiple of N >= segments */
    segments2 += n;            /* (yes, this is a moronic way to find that) */

  insegs  = ((float) (segments2 / n) + 0.5) / thickness;
  outsegs = (segments2 / n) - insegs;
  if (insegs  <= 0) insegs = 1;
  if (outsegs <= 0) outsegs = 1;

  segments2 = (insegs + outsegs) * n;
  width = M_PI * 2 / segments2;

  tick = 0;
  state = 0;
  for (i = 0; i < segments2; i++, tick++)
    {
      GLfloat th1 = i * width;
      GLfloat th2 = th1 + width;
      GLfloat cth1 = cos(th1);
      GLfloat sth1 = sin(th1);
      GLfloat cth2 = cos(th2);
      GLfloat sth2 = sin(th2);
      GLfloat orb = rb;

      int changed = (i == 0);

      if (state == 0 && tick == insegs)
        tick = 0, state = 1, changed = 1;
      else if (state == 1 && tick == outsegs)
        tick = 0, state = 0, changed = 1;

      if ((state == 1 ||                /* in */
           (state == 0 && changed)) &&
          (!wire_p || wire_all_p))
        {
          /* top */
          glFrontFace (GL_CCW);
          glBegin (wire_p ? GL_LINES : GL_QUADS);
          glNormal3f (0, 0, -1);
          glVertex3f (cth1 * ra, sth1 * ra, z1);
          glVertex3f (cth1 * rb, sth1 * rb, z1);
          glVertex3f (cth2 * rb, sth2 * rb, z1);
          glVertex3f (cth2 * ra, sth2 * ra, z1);
          polys++;
          glEnd();

          /* bottom */
          glFrontFace (GL_CW);
          glBegin (wire_p ? GL_LINES : GL_QUADS);
          glNormal3f (0, 0, 1);
          glVertex3f (cth1 * ra, sth1 * ra, z2);
          glVertex3f (cth1 * rb, sth1 * rb, z2);
          glVertex3f (cth2 * rb, sth2 * rb, z2);
          glVertex3f (cth2 * ra, sth2 * ra, z2);
          polys++;
          glEnd();
        }

      if (state == 1 && changed)   /* entering "in" state */
        {
          /* left */
          glFrontFace (GL_CW);
          glBegin (wire_p ? GL_LINES : GL_QUADS);
          do_normal (cth1 * rb, sth1 * rb, z1,
                     cth1 * ra, sth1 * ra, z1,
                     cth1 * rb, sth1 * rb, z2);
          glVertex3f (cth1 * ra, sth1 * ra, z1);
          glVertex3f (cth1 * rb, sth1 * rb, z1);
          glVertex3f (cth1 * rb, sth1 * rb, z2);
          glVertex3f (cth1 * ra, sth1 * ra, z2);
          polys++;
          glEnd();
        }

      if (state == 0 && changed)   /* entering "out" state */
        {
          /* right */
          glFrontFace (GL_CCW);
          glBegin (wire_p ? GL_LINES : GL_QUADS);
          do_normal (cth2 * ra, sth2 * ra, z1,
                     cth2 * rb, sth2 * rb, z1,
                     cth2 * rb, sth2 * rb, z2);
          glVertex3f (cth2 * ra, sth2 * ra, z1);
          glVertex3f (cth2 * rb, sth2 * rb, z1);
          glVertex3f (cth2 * rb, sth2 * rb, z2);
          glVertex3f (cth2 * ra, sth2 * ra, z2);
          polys++;
          glEnd();
        }

      rb = orb;
    }
  glEnd();
  return polys;
}


/* Draws some bumps (embedded cylinders) on the gear.
 */
static int
draw_gear_nubs (ModeInfo *mi, gear *g)
{
  Bool wire_p = MI_IS_WIREFRAME(mi);
  int polys = 0;
  int i;
  int steps = (g->size != LARGE ? 5 : 20);
  double r, size, height;
  GLfloat *cc;
  int which;
  GLfloat width, off;

  if (! g->nubs) return 0;

  which = biggest_ring (g, &r, &size, &height);
  size /= 5;
  height *= 0.7;

  cc = (which == 1 ? g->color : g->color2);
  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, cc);

  width = M_PI * 2 / g->nubs;
  off = M_PI / (g->nteeth * 2);  /* align first nub with a tooth */

  for (i = 0; i < g->nubs; i++)
    {
      GLfloat th = (i * width) + off;
      glPushMatrix();
      glTranslatef (cos(th) * r, sin(th) * r, 0);

      if (wire_p && !wire_all_p)
        polys += draw_ring (mi, (g->size == LARGE ? steps/2 : steps),
                            size, 0, 0, False);
      else
        {
          polys += draw_disc (mi, steps, 0, size, -height, True);
          polys += draw_disc (mi, steps, 0, size,  height, False);
          polys += draw_ring (mi, steps, size, -height, height, False);
        }
      glPopMatrix();
    }
  return polys;
}



/* Draws a much simpler representation of a gear.
 */
static int
draw_gear_schematic (ModeInfo *mi, gear *g)
{
  Bool wire_p = MI_IS_WIREFRAME(mi);
  int polys = 0;
  int i;
  GLfloat width = M_PI * 2 / g->nteeth;

  if (!wire_p) glDisable(GL_LIGHTING);
  glColor3f (g->color[0] * 0.6, g->color[1] * 0.6, g->color[2] * 0.6);

  glBegin (GL_LINES);
  for (i = 0; i < g->nteeth; i++)
    {
      GLfloat th = (i * width) + (width/4);
      glVertex3f (0, 0, -g->thickness/2);
      glVertex3f (cos(th) * g->r, sin(th) * g->r, -g->thickness/2);
    }
  polys += g->nteeth;
  glEnd();

  glBegin (GL_LINE_LOOP);
  for (i = 0; i < g->nteeth; i++)
    {
      GLfloat th = (i * width) + (width/4);
      glVertex3f (cos(th) * g->r, sin(th) * g->r, -g->thickness/2);
    }
  polys += g->nteeth;
  glEnd();

  if (!wire_p) glEnable(GL_LIGHTING);
  return polys;
}


/* Renders all the interior (non-toothy) parts of a gear:
   the discs, axles, etc.
 */
static int
draw_gear_interior (ModeInfo *mi, gear *g)
{
  Bool wire_p = MI_IS_WIREFRAME(mi);
  int polys = 0;

  int steps = g->nteeth * 2;
  if (steps < 10) steps = 10;
  if ((wire_p && !wire_all_p) || g->size != LARGE) steps /= 2;
  if (g->size != LARGE && steps > 16) steps = 16;

  /* ring 1 (facing in) is done in draw_gear_teeth */

  /* ring 2 (facing in) and disc 2
   */
  if (g->inner_r2)
    {
      GLfloat ra = g->inner_r * 1.04;  /* slightly larger than inner_r */
      GLfloat rb = g->inner_r2;        /*  since the points don't line up */
      GLfloat za = -g->thickness2/2;
      GLfloat zb =  g->thickness2/2;

      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, g->color2);

      if ((g->coax_p != 1 && !g->inner_r3) ||
          (wire_p && wire_all_p))
        polys += draw_ring (mi, steps, rb, za, zb, True);  /* ring facing in */

      if (wire_p && wire_all_p)
        polys += draw_ring (mi, steps, ra, za, zb, True);  /* ring facing in */

      if (g->spokes)
        polys += draw_spokes (mi, g->spokes, g->spoke_thickness,
                              steps, ra, rb, za, zb);
      else if (!wire_p || wire_all_p)
        {
          polys += draw_disc (mi, steps, ra, rb, za, True);  /* top plate */
          polys += draw_disc (mi, steps, ra, rb, zb, False); /* bottom plate */
        }
    }

  /* ring 3 (facing in and out) and disc 3
   */
  if (g->inner_r3)
    {
      GLfloat ra = g->inner_r2;
      GLfloat rb = g->inner_r3;
      GLfloat za = -g->thickness3/2;
      GLfloat zb =  g->thickness3/2;

      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, g->color);

      polys += draw_ring (mi, steps, ra, za, zb, False);  /* ring facing out */

      if (g->coax_p != 1 || (wire_p && wire_all_p))
        polys += draw_ring (mi, steps, rb, za, zb, True);  /* ring facing in */

      if (!wire_p || wire_all_p)
        {
          polys += draw_disc (mi, steps, ra, rb, za, True);  /* top plate */
          polys += draw_disc (mi, steps, ra, rb, zb, False); /* bottom plate */
        }
    }

  /* axle tube
   */
  if (g->coax_p == 1)
    {
      GLfloat cap_height = g->coax_thickness/3;

      GLfloat ra = (g->inner_r3 ? g->inner_r3 :
                    g->inner_r2 ? g->inner_r2 :
                    g->inner_r);
      GLfloat za = -(g->thickness/2 + cap_height);
      GLfloat zb = g->coax_thickness/2 + plane_displacement + cap_height;

      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, g->color);

      if (wire_p && !wire_all_p) steps /= 2;

      polys += draw_ring (mi, steps, ra, za, zb, False);  /* ring facing out */

      if (!wire_p || wire_all_p)
        {
          polys += draw_disc (mi, steps, 0,  ra, za, True);  /* top plate */
          polys += draw_disc (mi, steps, 0,  ra, zb, False); /* bottom plate */
        }
    }
  return polys;
}


/* gear_teeth_geometry computes the vertices and normals of the teeth
   of a gear.  This is the heavy lifting: there are a ton of polygons
   around the perimiter of a gear, and the normals are difficult (not
   radial or right angles.)

   It would be nice if we could cache this, but the numbers are
   different for essentially every gear:

      - Every gear has a different inner_r, so the vertices of the
        inner ring (and thus, the triangle fans on the top and bottom
        faces) are different in a non-scalable way.

      - If the ratio between tooth_w and tooth_h changes, the normals
        on the outside edges of the teeth are invalid (this can happen
        every time we start a new train.)

   So instead, we rely on OpenGL display lists to do the cacheing for
   us -- we only compute all these normals once per gear, instead of
   once per gear per frame.
 */

typedef struct {
  int npoints;
  XYZ *points;
  XYZ *fnormals;  /* face normals */
  XYZ *pnormals;  /* point normals */
} tooth_face;


static void
tooth_normals (tooth_face *f)
{
  int i;

  /* Compute the face normals for each facet. */
  for (i = 0; i < f->npoints; i++)
    {
      XYZ p1, p2, p3;
      int a = i;
      int b = (i == f->npoints-1 ? 0 : i+1);
      p1 = f->points[a];
      p2 = f->points[b];
      p3 = p1;
      p3.z++;
      f->fnormals[i] = calc_normal (p1, p2, p3);
    }

  /* From the face normals, compute the vertex normals
     (by averaging the normals of adjascent faces.)
   */
  for (i = 0; i < f->npoints; i++)
    {
      int a = (i == 0 ? f->npoints-1 : i-1);
      int b = i;
      XYZ n1 = f->fnormals[a];   /* normal of [i-1 - i] face */
      XYZ n2 = f->fnormals[b];   /* normal of [i - i+1] face */
      f->pnormals[i].x = (n1.x + n2.x) / 2;
      f->pnormals[i].y = (n1.y + n2.y) / 2;
      f->pnormals[i].z = (n1.z + n2.z) / 2;
    }
}


static void
gear_teeth_geometry (ModeInfo *mi, gear *g,
                     tooth_face *orim,      /* outer rim (the teeth) */
                     tooth_face *irim)      /* inner rim (the hole)  */
{
  int i;
  int ppt = 9;   /* max points per tooth */
  GLfloat width = M_PI * 2 / g->nteeth;
  GLfloat rh = g->tooth_h;
  GLfloat tw = width;

  /* Approximate shape of an "involute" gear tooth.

                                 (TH)
                 th0 th1 th2 th3 th4 th5 th6 th7 th8   th9    th10
                   :  :  :   :    :    :   :  :  :      :      :
                   :  :  :   :    :    :   :  :  :      :      :
        r0 ........:..:..:...___________...:..:..:......:......:..
                   :  :  :  /:    :    :\  :  :  :      :      :
                   :  :  : / :    :    : \ :  :  :      :      :
                   :  :  :/  :    :    :  \:  :  :      :      :
        r1 ........:.....@...:....:....:...@..:..:......:......:..
                   :  : @:   :    :    :   :@ :  :      :      :
    (R) ...........:...@.:...:....:....:...:.@..........:......:......
                   :  :@ :   :    :    :   : @:  :      :      :
        r2 ........:..@..:...:....:....:...:..@:........:......:..
                   : /:  :   :    :    :   :  :\ :      :      :
                   :/ :  :   :    :    :   :  : \:      :      : /
        r3 ......__/..:..:...:....:....:...:..:..\______________/
                   :  :  :   :    :    :   :  :  :      :      :
                   |  :  :   :    :    :   :  :  |      :      :
                   :  :  :   :    :    :   :  :  :      :      :
                   |  :  :   :    :    :   :  :  |      :      :
        r4 ......__:_____________________________:________________
   */

  GLfloat r[20];
  GLfloat th[20];
  GLfloat R = g->r;

  r[0] = R + (rh * 0.5);
  r[1] = R + (rh * 0.25);
  r[2] = R - (r[1]-R);
  r[3] = R - (r[0]-R);
  r[4] = g->inner_r;

  th[0] = -tw * (g->size == SMALL ? 0.5 : g->size == MEDIUM ? 0.41 : 0.45);
  th[1] = -tw * 0.30;
  th[2] = -tw * (g->nteeth >= 5 ? 0.16 : 0.12);
  th[3] = -tw * (g->size == MEDIUM ? 0.1 : 0.04);
  th[4] =  0;
  th[5] =  -th[3];
  th[6] =  -th[2];
  th[7] =  -th[1];
  th[8] =  -th[0];
  th[9] =  width / 2;
  th[10]=  th[0] + width;

  orim->npoints  = 0;
  orim->points   = (XYZ *) calloc(ppt * g->nteeth+1, sizeof(*orim->points));
  orim->fnormals = (XYZ *) calloc(ppt * g->nteeth+1, sizeof(*orim->fnormals));
  orim->pnormals = (XYZ *) calloc(ppt * g->nteeth+1, sizeof(*orim->pnormals));

  irim->npoints  = 0;
  irim->points   = (XYZ *) calloc(ppt * g->nteeth+1, sizeof(*irim->points));
  irim->fnormals = (XYZ *) calloc(ppt * g->nteeth+1, sizeof(*irim->fnormals));
  irim->pnormals = (XYZ *) calloc(ppt * g->nteeth+1, sizeof(*irim->pnormals));

  if (!orim->points || !orim->pnormals || !orim->fnormals ||
      !irim->points || !irim->pnormals || !irim->fnormals)
    {
      fprintf (stderr, "%s: out of memory\n", progname);
      exit (1);
    }

  /* First, compute the coordinates of every point used for every tooth.
   */
  for (i = 0; i < g->nteeth; i++)
    {
      GLfloat TH = (i * width) + (width/4);

#     undef PUSH
#     define PUSH(OPR,IPR,PTH) \
        orim->points[orim->npoints].x = cos(TH+th[(PTH)]) * r[(OPR)]; \
        orim->points[orim->npoints].y = sin(TH+th[(PTH)]) * r[(OPR)]; \
        orim->npoints++; \
        irim->points[irim->npoints].x = cos(TH+th[(PTH)]) * r[(IPR)]; \
        irim->points[irim->npoints].y = sin(TH+th[(PTH)]) * r[(IPR)]; \
        irim->npoints++

      if (g->size == SMALL)
        {
          PUSH(3, 4, 0);       /* tooth left 1 */
          PUSH(0, 4, 4);       /* tooth top middle */
        }
      else if (g->size == MEDIUM)
        {
          PUSH(3, 4, 0);       /* tooth left 1 */
          PUSH(0, 4, 3);       /* tooth top left */
          PUSH(0, 4, 5);       /* tooth top right */
          PUSH(3, 4, 8);       /* tooth right 3 */
        }
      else if (g->size == LARGE)
        {
          PUSH(3, 4, 0);       /* tooth left 1 */
          PUSH(2, 4, 1);       /* tooth left 2 */
          PUSH(1, 4, 2);       /* tooth left 3 */
          PUSH(0, 4, 3);       /* tooth top left */
          PUSH(0, 4, 5);       /* tooth top right */
          PUSH(1, 4, 6);       /* tooth right 1 */
          PUSH(2, 4, 7);       /* tooth right 2 */
          PUSH(3, 4, 8);       /* tooth right 3 */
          PUSH(3, 4, 9);       /* gap top */
        }
      else
        abort();
#     undef PUSH

      if (i == 0 && orim->npoints > ppt) abort();  /* go update "ppt"! */
    }

  tooth_normals (orim);
  tooth_normals (irim);
}


/* Renders all teeth of a gear.
 */
static int
draw_gear_teeth (ModeInfo *mi, gear *g)
{
  Bool wire_p = MI_IS_WIREFRAME(mi);
  Bool show_normals_p = False;
  int polys = 0;
  int i;

  GLfloat z1 = -g->thickness/2;
  GLfloat z2 =  g->thickness/2;

  tooth_face orim, irim;
  gear_teeth_geometry (mi, g, &orim, &irim);

  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, g->color);

  /* Draw the outer rim (the teeth)
     (In wire mode, this draws just the upright lines.)
   */
  glFrontFace (GL_CW);
  glBegin (wire_p ? GL_LINES : GL_QUAD_STRIP);
  for (i = 0; i < orim.npoints; i++)
    {
      glNormal3f (orim.pnormals[i].x, orim.pnormals[i].y, orim.pnormals[i].z);
      glVertex3f (orim.points[i].x, orim.points[i].y, z1);
      glVertex3f (orim.points[i].x, orim.points[i].y, z2);

      /* Show the face normal vectors */
      if (wire_p && show_normals_p)
        {
          XYZ n = orim.fnormals[i];
          int a = i;
          int b = (i == orim.npoints-1 ? 0 : i+1);
          GLfloat x = (orim.points[a].x + orim.points[b].x) / 2;
          GLfloat y = (orim.points[a].y + orim.points[b].y) / 2;
          GLfloat z  = (z1 + z2) / 2;
          glVertex3f (x, y, z);
          glVertex3f (x + n.x, y + n.y, z);
        }

      /* Show the vertex normal vectors */
      if (wire_p && show_normals_p)
        {
          XYZ n = orim.pnormals[i];
          GLfloat x = orim.points[i].x;
          GLfloat y = orim.points[i].y;
          GLfloat z  = (z1 + z2) / 2;
          glVertex3f (x, y, z);
          glVertex3f (x + n.x, y + n.y, z);
        }
    }

  if (!wire_p)  /* close the quad loop */
    {
      glNormal3f (orim.pnormals[0].x, orim.pnormals[0].y, orim.pnormals[0].z);
      glVertex3f (orim.points[0].x, orim.points[0].y, z1);
      glVertex3f (orim.points[0].x, orim.points[0].y, z2);
    }
  polys += orim.npoints;
  glEnd();

  /* Draw the outer rim circles, in wire mode */
  if (wire_p)
    {
      glBegin (GL_LINE_LOOP);
      for (i = 0; i < orim.npoints; i++)
        glVertex3f (orim.points[i].x, orim.points[i].y, z1);
      glEnd();
      glBegin (GL_LINE_LOOP);
      for (i = 0; i < orim.npoints; i++)
        glVertex3f (orim.points[i].x, orim.points[i].y, z2);
      glEnd();
    }


  /* Draw the inner rim (the hole)
     (In wire mode, this draws just the upright lines.)
   */
  glFrontFace (GL_CCW);
  glBegin (wire_p ? GL_LINES : GL_QUAD_STRIP);
  for (i = 0; i < irim.npoints; i++)
    {
      glNormal3f(-irim.pnormals[i].x, -irim.pnormals[i].y,-irim.pnormals[i].z);
      glVertex3f (irim.points[i].x, irim.points[i].y, z1);
      glVertex3f (irim.points[i].x, irim.points[i].y, z2);

      /* Show the face normal vectors */
      if (wire_p && show_normals_p)
        {
          XYZ n = irim.fnormals[i];
          int a = i;
          int b = (i == irim.npoints-1 ? 0 : i+1);
          GLfloat x = (irim.points[a].x + irim.points[b].x) / 2;
          GLfloat y = (irim.points[a].y + irim.points[b].y) / 2;
          GLfloat z  = (z1 + z2) / 2;
          glVertex3f (x, y, z);
          glVertex3f (x - n.x, y - n.y, z);
        }

      /* Show the vertex normal vectors */
      if (wire_p && show_normals_p)
        {
          XYZ n = irim.pnormals[i];
          GLfloat x = irim.points[i].x;
          GLfloat y = irim.points[i].y;
          GLfloat z  = (z1 + z2) / 2;
          glVertex3f (x, y, z);
          glVertex3f (x - n.x, y - n.y, z);
        }
    }

  if (!wire_p)  /* close the quad loop */
    {
      glNormal3f (-irim.pnormals[0].x,-irim.pnormals[0].y,-irim.pnormals[0].z);
      glVertex3f (irim.points[0].x, irim.points[0].y, z1);
      glVertex3f (irim.points[0].x, irim.points[0].y, z2);
    }
  polys += irim.npoints;
  glEnd();

  /* Draw the inner rim circles, in wire mode
   */
  if (wire_p)
    {
      glBegin (GL_LINE_LOOP);
      for (i = 0; i < irim.npoints; i++)
        glVertex3f (irim.points[i].x, irim.points[i].y, z1);
      glEnd();
      glBegin (GL_LINE_LOOP);
      for (i = 0; i < irim.npoints; i++)
        glVertex3f (irim.points[i].x, irim.points[i].y, z2);
      glEnd();
    }


  /* Draw the side (the flat bit)
   */
  if (!wire_p || wire_all_p)
    {
      GLfloat z;
      if (irim.npoints != orim.npoints) abort();
      for (z = z1; z <= z2; z += z2-z1)
        {
          glFrontFace (z == z1 ? GL_CCW : GL_CW);
          glNormal3f (0, 0, z);
          glBegin (wire_p ? GL_LINES : GL_QUAD_STRIP);
          for (i = 0; i < orim.npoints; i++)
            {
              glVertex3f (orim.points[i].x, orim.points[i].y, z);
              glVertex3f (irim.points[i].x, irim.points[i].y, z);
            }
          if (!wire_p)  /* close the quad loop */
            {
              glVertex3f (orim.points[0].x, orim.points[0].y, z);
              glVertex3f (irim.points[0].x, irim.points[0].y, z);
            }
          polys += orim.npoints;
          glEnd();
        }
    }

  free (irim.points);
  free (irim.fnormals);
  free (irim.pnormals);

  free (orim.points);
  free (orim.fnormals);
  free (orim.pnormals);

  return polys;
}


/* Render one gear, unrotated at 0,0.
 */
static int
draw_gear_1 (ModeInfo *mi, gear *g)
{
  Bool wire_p = MI_IS_WIREFRAME(mi);
  int polys = 0;

  static GLfloat spec[4] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat shiny   = 128.0;

  glMaterialfv (GL_FRONT, GL_SPECULAR,  spec);
  glMateriali  (GL_FRONT, GL_SHININESS, shiny);
  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, g->color);
  glColor3f (g->color[0], g->color[1], g->color[2]);

  if (debug_p && wire_p)
    polys += draw_gear_schematic (mi, g);
  else
    {
      glPushMatrix();
      glRotatef (g->wobble, 1, 0, 0);
      polys += draw_gear_teeth (mi, g);
      polys += draw_gear_interior (mi, g);
      polys += draw_gear_nubs (mi, g);
      glPopMatrix();
    }
  return polys;
}


/* Render one gear in the proper position, creating the gear's
   display list first if necessary.
 */
static void
draw_gear (ModeInfo *mi, int which)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  gear *g = pp->gears[which];
  GLfloat th;

  Bool visible_p = (g->x + g->r + g->tooth_h >= pp->render_left &&
                    g->x - g->r - g->tooth_h <= pp->render_right);

  if (!visible_p && !debug_p)
    return;

  if (! g->dlist)
    {
      g->dlist = glGenLists (1);
      if (! g->dlist)
        {
          /* I don't know how many display lists a GL implementation
             is supposed to provide, but hopefully it's more than
             "a few hundred", or we'll be in trouble...
           */
          check_gl_error ("glGenLists");
          abort();
        }

      glNewList (g->dlist, GL_COMPILE);
      g->polygons = draw_gear_1 (mi, g);
      glEndList ();
    }

  glPushMatrix();

  glTranslatef (g->x, g->y, g->z);

  if (g->motion_blur_p && !pp->button_down_p)
    {
      /* If we're in motion-blur mode, then draw the gear so that each
         frame rotates it by exactly one half tooth-width, so that it
         looks flickery around the edges.  But, revert to the normal
         way when the mouse button is down lest the user see overlapping
         polygons.
       */
      th = g->motion_blur_p * 180.0 / g->nteeth * (g->th > 0 ? 1 : -1);
      g->motion_blur_p++;
    }
  else
    th = g->th;

  glRotatef (th, 0, 0, 1);

  glPushName (g->id);

  if (! visible_p)
    mi->polygon_count += draw_gear_schematic (mi, g);
  else
    {
      glCallList (g->dlist);
      mi->polygon_count += g->polygons;
    }

  glPopName();
  glPopMatrix();
}


/* Render all gears.
 */
static void
draw_gears (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  Bool wire_p = MI_IS_WIREFRAME(mi);
  int i;

  glColor4f (1, 1, 0.8, 1);

  glInitNames();

  for (i = 0; i < pp->ngears; i++)
    draw_gear (mi, i);

  /* draw a line connecting gears that are, uh, geared. */
  if (debug_p)
    {
      static GLfloat color[4] = {1.0, 0.0, 0.0, 1.0};
      GLfloat off = 0.1;
      GLfloat ox=0, oy=0, oz=0;

      if (!wire_p) glDisable(GL_LIGHTING);
      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);
      glColor3f (color[0], color[1], color[2]);

      for (i = 0; i < pp->ngears; i++)
        {
          gear *g = pp->gears[i];
          glBegin(GL_LINE_STRIP);
          glVertex3f (g->x, g->y, g->z - off);
          glVertex3f (g->x, g->y, g->z + off);
          if (i > 0 && !g->base_p)
            glVertex3f (ox, oy, oz + off);
          glEnd();
          ox = g->x;
          oy = g->y;
          oz = g->z;
        }
      if (!wire_p) glEnable(GL_LIGHTING);
    }
}


/* Mouse hit detection
 */
void
find_mouse_gear (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  int screen_width = MI_WIDTH (mi);
  int screen_height = MI_HEIGHT (mi);
  GLfloat h = (GLfloat) screen_height / (GLfloat) screen_width;
  int x, y;
  int hits;

  pp->mouse_gear_id = 0;

  /* Poll mouse position */
  {
    Window r, c;
    int rx, ry;
    unsigned int m;
    XQueryPointer (MI_DISPLAY (mi), MI_WINDOW (mi),
                   &r, &c, &rx, &ry, &x, &y, &m);
  }

  if (x < 0 || y < 0 || x > screen_width || y > screen_height)
    return;  /* out of window */

  /* Run OpenGL hit detector */
  {
    GLint vp[4];
    GLuint selbuf[512];

    glSelectBuffer (sizeof(selbuf), selbuf);  /* set up "select" mode */
    glRenderMode (GL_SELECT);
    glMatrixMode (GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glGetIntegerv (GL_VIEWPORT, vp);         /* save old vp */
    gluPickMatrix (x, vp[3]-y, 5, 5, vp);
    gluPerspective (30.0, 1/h, 1.0, 100.0);  /* must match reshape_pinion() */
    glMatrixMode (GL_MODELVIEW);

    draw_gears (mi);                         /* render into "select" buffer */

    glMatrixMode (GL_PROJECTION);            /* restore old vp */
    glPopMatrix ();
    glMatrixMode (GL_MODELVIEW);
    glFlush();
    hits = glRenderMode (GL_RENDER);         /* done selecting */

    if (hits > 0)
      {
        int i;
        GLuint name_count = 0;
        GLuint *p = (GLuint *) selbuf;
        GLuint *pnames = 0;
        GLuint min_z = ~0;

        for (i = 0; i < hits; i++)
          {     
            int names = *p++;
            if (*p < min_z)                  /* find match closest to screen */
              {
                name_count = names;
                min_z = *p;
                pnames = p+2;
              }
            p += names+2;
          }

        if (name_count > 0)                  /* take first hit */
          pp->mouse_gear_id = pnames[0];
      }
  }
}


/* Window management, etc
 */
void
reshape_pinion (ModeInfo *mi, int width, int height)
{
  GLfloat h = (GLfloat) height / (GLfloat) width;
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];

  glViewport (0, 0, (GLint) width, (GLint) height);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective (30.0, 1/h, 1.0, 100.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt( 0.0, 0.0, 30.0,
             0.0, 0.0, 0.0,
             0.0, 1.0, 0.0);

  glClear(GL_COLOR_BUFFER_BIT);

  {
    GLfloat render_width, layout_width;
    pp->vp_height = 1.0;
    pp->vp_width  = 1/h;

    pp->vp_left   = -pp->vp_width/2;
    pp->vp_right  =  pp->vp_width/2;
    pp->vp_top    =  pp->vp_height/2;
    pp->vp_bottom = -pp->vp_height/2;

    render_width = pp->vp_width * 2;
    layout_width = pp->vp_width * 0.8 * gear_size;

    pp->render_left  = -render_width/2;
    pp->render_right =  render_width/2;

    pp->layout_left  = pp->render_right;
    pp->layout_right = pp->layout_left + layout_width;
  }
}


Bool
pinion_handle_event (ModeInfo *mi, XEvent *event)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];

  if (event->xany.type == ButtonPress &&
      event->xbutton.button == Button1)
    {
      pp->button_down_p = True;
      gltrackball_start (pp->trackball,
                         event->xbutton.x, event->xbutton.y,
                         MI_WIDTH (mi), MI_HEIGHT (mi));
      return True;
    }
  else if (event->xany.type == ButtonRelease &&
           event->xbutton.button == Button1)
    {
      pp->button_down_p = False;
      return True;
    }
  else if (event->xany.type == ButtonPress &&
           (event->xbutton.button == Button4 ||
            event->xbutton.button == Button5))
    {
      gltrackball_mousewheel (pp->trackball, event->xbutton.button, 5,
                              !!event->xbutton.state);
      return True;
    }
  else if (event->xany.type == MotionNotify &&
           pp->button_down_p)
    {
      gltrackball_track (pp->trackball,
                         event->xmotion.x, event->xmotion.y,
                         MI_WIDTH (mi), MI_HEIGHT (mi));
      return True;
    }
  else if (event->xany.type == KeyPress)
    {
      KeySym keysym;
      char c = 0;
      XLookupString (&event->xkey, &c, 1, &keysym, 0);
      if (c == ' ' && debug_one_gear_p && pp->ngears)
        {
          delete_gear (mi, pp->gears[0]);
          return True;
        }
    }

  return False;
}


void 
init_pinion (ModeInfo *mi)
{
  pinion_configuration *pp;
  int wire = MI_IS_WIREFRAME(mi);

  if (!pps) {
    pps = (pinion_configuration *)
      calloc (MI_NUM_SCREENS(mi), sizeof (pinion_configuration));
    if (!pps) {
      fprintf(stderr, "%s: out of memory\n", progname);
      exit(1);
    }

    pp = &pps[MI_SCREEN(mi)];
  }

  pp = &pps[MI_SCREEN(mi)];

  pp->glx_context = init_GL(mi);

  load_fonts (mi);
  reshape_pinion (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  pp->title_list  = glGenLists (1);

  pp->ngears = 0;
  pp->gears_size = 0;
  pp->gears = 0;

  plane_displacement *= gear_size;

  if (!wire)
    {
      GLfloat pos[4] = {-3.0, 1.0, 1.0, 0.0};
      GLfloat amb[4] = { 0.0, 0.0, 0.0, 1.0};
      GLfloat dif[4] = { 1.0, 1.0, 1.0, 1.0};
      GLfloat spc[4] = { 1.0, 1.0, 1.0, 1.0};

      glEnable(GL_LIGHTING);
      glEnable(GL_LIGHT0);
      glEnable(GL_DEPTH_TEST);
      glEnable(GL_CULL_FACE);

      glLightfv(GL_LIGHT0, GL_POSITION, pos);
      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
    }

  pp->trackball = gltrackball_init ();

  ffwd (mi);
}


void
draw_pinion (ModeInfo *mi)
{
  pinion_configuration *pp = &pps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);
  Bool wire_p = MI_IS_WIREFRAME(mi);
  static int tick = 0;

  if (!pp->glx_context)
    return;

  if (!pp->button_down_p)
    {
      if (!debug_one_gear_p || pp->ngears == 0)
        scroll_gears (mi);
      spin_gears (mi);
    }

  glShadeModel(GL_SMOOTH);

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_NORMALIZE);
  glEnable(GL_CULL_FACE);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix ();
  {
    gltrackball_rotate (pp->trackball);
    mi->polygon_count = 0;

    glScalef (16, 16, 16);   /* map vp_width/height to the screen */

    if (debug_one_gear_p)    /* zoom in */
      glScalef (3, 3, 3);
    else if (debug_p)        /* show the "visible" and "layout" areas */
      {
        GLfloat ow = pp->layout_right - pp->render_left;
        GLfloat rw = pp->render_right - pp->render_left;
        GLfloat s = (pp->vp_width / ow) * 0.85;
        glScalef (s, s, s);
        glTranslatef (-(ow - rw) / 2, 0, 0);
      }
    else
      {
        GLfloat s = 1.2;
        glScalef (s, s, s);           /* zoom in a little more */
        glRotatef (-35, 1, 0, 0);     /* tilt back */
        glRotatef (  8, 0, 1, 0);     /* tilt left */
        glTranslatef (0.02, 0.1, 0);  /* pan up */
      }

    draw_gears (mi);

    if (debug_p)
      {
        if (!wire_p) glDisable(GL_LIGHTING);
        glColor3f (0.6, 0, 0);
        glBegin(GL_LINE_LOOP);
        glVertex3f (pp->render_left,  pp->vp_top,    0);
        glVertex3f (pp->render_right, pp->vp_top,    0);
        glVertex3f (pp->render_right, pp->vp_bottom, 0);
        glVertex3f (pp->render_left,  pp->vp_bottom, 0);
        glEnd();
        glColor3f (0.4, 0, 0);
        glBegin(GL_LINES);
        glVertex3f (pp->vp_left,      pp->vp_top,    0);
        glVertex3f (pp->vp_left,      pp->vp_bottom, 0);
        glVertex3f (pp->vp_right,     pp->vp_top,    0);
        glVertex3f (pp->vp_right,     pp->vp_bottom, 0);
        glEnd();
        glColor3f (0, 0.4, 0);
        glBegin(GL_LINE_LOOP);
        glVertex3f (pp->layout_left,  pp->vp_top,    0);
        glVertex3f (pp->layout_right, pp->vp_top,    0);
        glVertex3f (pp->layout_right, pp->vp_bottom, 0);
        glVertex3f (pp->layout_left,  pp->vp_bottom, 0);
        glEnd();
        if (!wire_p) glEnable(GL_LIGHTING);
      }

    if (tick++ > 10)   /* only do this every N frames */
      {
        tick = 0;
        find_mouse_gear (mi);
        new_label (mi);
      }
  }
  glPopMatrix ();

  glCallList (pp->title_list);

  if (mi->fps_p) do_fps (mi);
  glFinish();

  glXSwapBuffers(dpy, window);
}

#endif /* USE_GL */
