static char local_id[] = "$Id$";
/*
 * $Log$
 * Revision 1.75  2002/01/27 00:38:19  slevy
 * Add "setenv name value" data command.
 *
 * Revision 1.74  2002/01/23 03:56:34  slevy
 * Add "timealign", "skipblanktimes" commands.
 * Default change: no longer skip blank times!
 * Per-data time ranges now extend overall clock range,
 * don't replace it.
 * Reset material properties (shininess, specular, ambient)
 * when drawing a lighted mesh.
 *
 * Revision 1.73  2001/12/28 07:23:57  slevy
 * Move plugins (kira, warp, ieeeio) off to separate source files.
 * "where" command gives full-precision values, and reports jump-pos with scale.
 * Allow for model-specific render function (initially for Maya models).
 * env TXDEBUG.
 *
 * Revision 1.72  2001/11/09 05:27:53  slevy
 * In specks_set_timestep(), don't set clock's range if we only have
 * a single timestep ourselves!
 *
 * Revision 1.71  2001/11/08 06:32:52  slevy
 * Extract async code into async.c.
 * Add pointsize metering, normally #ifdef USE_PTRACK'ed out.
 *
 * Revision 1.70  2001/11/06 06:20:30  slevy
 * Centralize test for nonempty timestep slot: specks_nonempty_timestep().
 * Fix tokenize() for case of "quoted" or 'quoted' arg strings.
 *
 * Revision 1.69  2001/10/19 17:48:51  slevy
 * Don't leak FILE in specks_read_waveobj().
 *
 * Revision 1.68  2001/08/29 17:54:00  slevy
 * Make rejoinargs() work robustly -- use a private static malloced area,
 * don't overwrite args in memory.
 *
 * Revision 1.67  2001/08/28 18:27:34  slevy
 * Remove lots of unused variables.  Move some used only in CAVE code
 * inside #ifdef brackets.
 *
 * Revision 1.66  2001/08/28 02:05:22  slevy
 * Fix format string usage.
 *
 * Revision 1.65  2001/08/26 17:40:04  slevy
 * Bounds-check all references to st->meshes[][], st->annot[][], etc. --
 * all those maintained by specks_timespecksptr.  We no longer guarantee
 * that st->curtime is always in range 0..st->ntimes-1, so just yield NULL
 * for out-of-range references.  New CURDATATIME(fieldname) does it.
 *
 * Revision 1.64  2001/08/26 02:10:40  slevy
 * "color rgb ..." sets colors of otherwise-uncolored mesh objects.
 * Only call parti_parse_args() if non-CAVE.
 *
 * Revision 1.63  2001/08/24 01:02:38  slevy
 * Add polygonal meshes -- wavefront .obj models.
 *
 * Revision 1.62  2001/08/16 19:58:56  slevy
 * Pull non-specks-specific code out of partibrains.c
 * and into partiview.cc, using new parti_add_commands hook.
 *
 * Revision 1.61  2001/08/16 17:23:41  slevy
 * New parti_parse_args() for generic add-on commands.
 * "where" prints more information.
 *
 * Revision 1.60  2001/07/19 20:10:23  slevy
 * st->threshsel is now kept as a SEL_DEST.
 * Fix seldest2src(), not even wrong.
 * Use specks_reupdate() after changing thresh, so the reported
 * selcount should be right.
 * specks_rethresh(): keep track of whether anything changed,
 * and increment st->selseq if so.
 *
 * Revision 1.59  2001/07/18 19:24:57  slevy
 * parse_selexpr() returns bit-encoded value saying whether src or dest or
 * both were filled in.
 *
 * Revision 1.58  2001/07/17 17:28:08  slevy
 * Increase MAXPTSIZE to allow bigger dots!
 *
 * Revision 1.57  2001/07/16 17:58:37  slevy
 * Don't take spurious snapshot if "snapset" command gets -... args.
 * Revert to compatible texture behavior: texture -A is again the default,
 * use texture -O for "over"-style blending.
 *
 * Revision 1.56  2001/07/15 23:09:46  slevy
 * Um, let "wanted == 0" and SEL_USE indicate that we want to match all pcles.
 *
 * Revision 1.55  2001/07/15 22:55:14  slevy
 * Er, those 'leadc' parameters to seldest()/selsrc() are ints, not SelTokens.
 *
 * Revision 1.54  2001/07/12 21:42:25  slevy
 * Use new selcounts() function to report stats whenever we mention a SelOp.
 * ``sel <src>'' alone now works too, to just report match count.
 *
 * Revision 1.53  2001/07/10 17:18:05  slevy
 * threshsel is a src, not a dest-type SelOp.
 *
 * Revision 1.52  2001/07/09 23:46:56  slevy
 * A bit more ripening of selection system...
 * And, report match counts from "sel", "thresh", "emph".
 *
 * Revision 1.51  2001/07/07 15:35:49  slevy
 * Implement more (most?) of selection framework:
 *     selsrc(), seldest(), selname(), seltoken().
 * "thresh" now accepts "-s 'destexpr'".
 * Add new selthresh SelOp.
 *
 * Revision 1.50  2001/07/04 03:44:12  slevy
 * Add framework of set-selection stuff; not really usable yet.
 * Next: implement "see" in drawspecks().
 * Add "picked" callback which gets notified of all pick results.
 *
 * Revision 1.49  2001/06/30 18:14:22  slevy
 * Provide for storing another bit in rgba alpha field: EMPHBIT, for
 * emphasis tag.
 * Use new sfStrDrawTJ (with transform & justification) for text.
 * Yeow, handle polymax properly -- default == infinity!
 * Add "vcmap" for per-datafield colormaps.
 * Enable GL_SMOOTH mode to get texture colors to work right (huh? why do we need to?).
 * "tfm c"/"tfm w" choose cam vs. world (default) location for object tfms.
 *
 * Revision 1.48  2001/05/30 14:32:55  slevy
 * Add subcameras ("subcam" command).
 * popen() needs "r" not "rb" -- no such thing as binary mode.
 * kira_open() gets original filename if we can't find it, for error messaging.
 *
 * Revision 1.47  2001/05/15 12:18:57  slevy
 * New interface to dynamic-data routines.
 * Now, the only #ifdef KIRA/WARP needed in partibrains.c are
 * the data-command initialization routines.  All others,
 * including control-command parsing and specialized drawing,
 * is now via a function table.
 *
 * Revision 1.46  2001/05/14 15:51:34  slevy
 * Initialize new speckseq value whenever we make a new specklist.
 * Put colorseq/sizeseq/threshseq invalidation in kira_parti, not here,
 * since we needn't do it when warping.
 * By default, clock range is whatever it had been set to before.
 *
 * Revision 1.45  2001/05/12 07:22:24  slevy
 * Add get-time-range func for dynamic data.
 * Initialize all dynamic-data stuff explicitly.
 *
 * Revision 1.44  2001/05/11 10:05:38  slevy
 * Add "warp" command if -DUSE_WARP.
 * Ellipsoids allow 3-component (Rx Ry Rz) orientation numbers too.
 * st->dyndata doesn't mean we should trash the anima[][] specklist.
 * Need to keep it intact for warping.
 *
 * Revision 1.43  2001/05/02 09:51:01  slevy
 * Add "ellipsoids" and "meshes" commands to toggle their display.
 * Finally parse snapshot arguments properly...?
 *
 * Revision 1.42  2001/04/26 08:52:46  slevy
 * Add hook for off-screen rendering: "-w width[xheight]" option for
 * snapshot/snapset.  Not actually implemented yet.
 * Add "clipbox hide".  This (a) doesn't display the big yellow box
 * and (b) doesn't enable OpenGL clipping -- just whatever object culling
 * the draw routine does anyway.
 * specks_draw_mesh() now pays attention to drawing style (solid/line/plane/point).
 * When drawing ellipsoids, disable blending altogether if bgcolor != 0.
 * Small, distant labels now disappear by default.  To get the old
 * draw-tiny-line behavior, use a negative "labelmin" value.
 *
 * Revision 1.41  2001/04/20 13:47:39  slevy
 * Allow *cment commands to alter multiple colormap entries: new editcmap().
 *
 * Revision 1.40  2001/04/10 19:18:33  slevy
 * Open sdb files (and while we're at it, other files too) in binary mode.
 *
 * Revision 1.39  2001/04/04 20:33:02  slevy
 * Use findfile() for "read" ctrl-command.
 *
 * Revision 1.38  2001/03/30 16:49:00  slevy
 * Change enum SurfStyle to avoid mentioning POINT, which Windows uses too.
 *
 * Revision 1.37  2001/03/30 14:00:42  slevy
 * Allow cment/boxcment/textcment as data commands too.
 * Add "ghosts" ctl command (not really implemented).
 * Add "winsize" ctl command.
 * Ellipsoids now accept -l levelno option, in which case
 * "hide levelno"/"show levelno" applies to them.  Default is -1,
 * which means "show if any level is shown", but hidden by "hide all".
 * Scrap ghost maintenance.  Do that elsewhere instead.
 * Make "maxcomment" work properly.
 *
 * Revision 1.36  2001/03/19 11:55:08  slevy
 * New version.c (derived from ../VERSION) contains current version string.
 * New "version" command in partibrains reports that and the partibrains.c CVS ver no.
 *
 * Revision 1.35  2001/03/19 10:39:07  slevy
 * Handle speck comments properly.
 *
 * Revision 1.34  2001/03/15 18:19:06  slevy
 * Don't include comments in argc/argv -- it complicates things.
 * Have a separate "comment" pointer.  Pass it to specks_read_ellipsoid too.
 *
 * Revision 1.33  2001/03/15 15:37:06  slevy
 * Yeow -- handle VIRDIR prefix properly!
 * Complain of unrecognized data commands.
 *
 * Revision 1.32  2001/03/14 17:27:05  slevy
 * Make rejoinargs() work -- don't omit last arg.
 * "object" command alone reports our currently selected object.
 *
 * Revision 1.31  2001/03/13 22:45:08  slevy
 * Add "verbose" flag to parti_allobjs, so "gall -v ..."
 * reports object name before invoking each command.
 *
 * Revision 1.30  2001/03/13 08:23:39  slevy
 * Use parti_object's new "create-if-not-present" flag.
 * Data-language references can create, command-language ones can't.
 * Switch specks_read() to argc/argv style, using new "tokenize()" function.
 * Some commands still want the original string, so we also have rejoinargs().
 * Allow for adjustable comment length with "maxcomment" data command.
 * New "ellipsoid" and "mesh" objects.  Only quadmeshes implemented right now.
 * Data-language "tfm" command is now quiet.  Command-language tfm still verbose.
 * Check at run time for endian-config errors and refuse to run if wrong!
 *
 * Revision 1.29  2001/03/08 22:02:29  slevy
 * Disable the parti menu unless PARTIMENU envar set.
 * Allow alpha to adjust brightness of mesh objects.
 *
 * Revision 1.28  2001/03/05 03:04:41  slevy
 * Add mesh objects.  Or, quad meshes, anyway.
 *
 * Revision 1.27  2001/03/04 16:47:35  slevy
 * Make "add" and "eval" behave consistently.
 * polyorivar and texturevar now accept field names as well as numbers.
 *
 * Revision 1.26  2001/02/22 20:04:34  slevy
 * CONSTify.
 *
 * Revision 1.25  2001/02/19 22:01:16  slevy
 * Satisfy windows C compiler: pull enum FadeModel outside struct speck, etc.
 *
 * Revision 1.24  2001/02/19 20:50:33  slevy
 * Oops, "object" should always invoke parti_object() whether there's an
 * alias or not!
 *
 * Revision 1.23  2001/02/17 22:02:54  slevy
 * Enlarge polygons so that unit circle is inscribed, not circumscribed.
 * Then "txscale .5" always shows entire texture regardless of polysides.
 * Add new "ptsize" command -- makes more sense than "fast".  "fast" still works.
 *
 * Revision 1.22  2001/02/17 17:44:05  slevy
 * For polygons, rotate circle of vertices by 1/2 step.
 * Then, for "polysides 4" and "txscale .707",
 * the vertices coincide with the corners of the 0..1 texture.
 *
 * Revision 1.21  2001/02/17 05:39:45  slevy
 * Allow (in data language) "object gN=NAME".
 *
 * Revision 1.20  2001/02/15 05:41:12  slevy
 * new textcmap, textcment commands.  Regularize color-gamma-mapping.
 * Object aliases: "object gN=ALIAS", or in command mode, "gN=ALIAS".
 * Accept "ellipsoid" data tag; not yet implemented.
 *
 * Revision 1.19  2001/02/05 00:41:52  slevy
 * Accept "time" as synonym for "step".
 * Add "pickrange".
 * Mention jump, center in help msg.
 *
 * Revision 1.18  2001/02/03 23:43:10  slevy
 * Don't let "kira tree" hide "kira track" -- demand 3 chars!
 *
 * Revision 1.17  2001/02/03 16:49:42  slevy
 * Update cookedcmap when "cment" changes cmap.
 *
 * Revision 1.16  2001/02/03 15:29:03  slevy
 * Toss unused variable.
 *
 * Revision 1.15  2001/02/03 14:50:44  slevy
 * Add "setgamma" (abbr. "setgam" or "cgam") command to adjust colors.
 * Add "kira tree {off|on|cross|tick} [tickscale]" subcommand
 * for showing tree structure of interacting groups.
 *
 * Revision 1.14  2001/01/31 17:11:54  slevy
 * Ensure that, for starlab, clock is always in "continuous" mode.
 *
 * Revision 1.13  2001/01/31 17:07:12  slevy
 * Add RCS Id and Log strings.
 *
 */

#define __USE_MISC	/* makes <math.h> define sqrtf() on GNU libc */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>

#if !sgi && !defined(sqrtf)
# define sqrtf(x)  sqrt(x)	/* if no sqrtf() */
#endif

#undef isspace		/* hack for irix 6.5 back-compat */
#undef isdigit
#undef isalnum

#if unix
# include <unistd.h>
# include <sys/types.h>
# include <netinet/in.h>  /* for htonl */
# include <time.h>
# include <alloca.h>
# ifndef WORDS_BIGENDIAN
#  include "config.h"	/* for WORDS_BIGENDIAN */
# endif
#else /*WIN32*/
# include "winjunk.h"
# define WORDS_BIGENDIAN 0
#endif
#include <string.h>
#include <errno.h>


#if WORDS_BIGENDIAN
#define RGBALPHA(rgb, alpha)	((rgb) | (alpha))
#define RGBWHITE		0xFFFFFF00
#define	PACKRGBA(r,g,b,a)	((r)<<24 | (g)<<16 | (b)<<8 | (a))
#else
#define RGBALPHA(rgb, alpha)	((rgb) | ((alpha)<<24))
#define RGBWHITE		0x00FFFFFF
#define	PACKRGBA(r,g,b,a)	((a)<<24 | (b)<<16 | (g)<<8 | (r))
#endif

#define	THRESHBIT		PACKRGBA(0,0,0,1)
#define	EMPHBIT			PACKRGBA(0,0,0,2)
#define	EXTRABITS		PACKRGBA(0,0,0,0xff)
#define	RGBBITS			PACKRGBA(0xff,0xff,0xff,0)

#if CAVE
# define  CAVEMENU  1
#endif

#include "geometry.h"
#include <GL/gl.h>

#if CAVEMENU
#include <cave_ogl.h>
#include "cavemenu.h"
#include "vd_util.h"
#define IFMENU(x)  (x)
#else
#define IFMENU(x)
#endif

#include "shmem.h"	/* NewN(), etc. */
#include "futil.h"

#include "specks.h"

#include "textures.h"
#include "findfile.h"
#include "partiviewc.h"
#include "sfont.h"

#include <sys/types.h>
#include <signal.h>

#ifdef sgi
#include <malloc.h>	/* for mallinfo(), amallinfo() */
#endif

#ifdef USE_KIRA
#include "kira_parti.h"
#endif

  /* Star Renderer (.sdb) structure -- from stardef.h */
typedef enum {ST_POINT, ST_BRIGHT_CLOUD ,ST_DARK_CLOUD, ST_BOTH_CLOUD, ST_OFF} stype;

typedef struct {
        float  x, y, z;
        float  dx, dy, dz;
        float  magnitude, radius;
        float  opacity;
        int  num;
        unsigned short  color;
        unsigned char   group;
        unsigned char   type;
} db_star;

typedef  struct  hrec { float  t;  int  num;}  hrec_t;
typedef  struct  mrec { float  mass, x, y, z, vx, vy, vz, rho, temp, sfr, gasmass;
                 int  id, token;}  mrec_t;
  /* end Star Renderer */


#define VDOT( v1, v2 )  ( (v1)->x[0]*(v2)->x[0] + (v1)->x[1]*(v2)->x[1] + (v1)->x[2]*(v2)->x[2] )

#if CAVEMENU
int parti_menuwall, parti_menubox[4];

static MenuHead *pmenu = NULL;
static MenuHead *stubmenu = NULL;

struct ments {
  MenuEnt *alpha, *point, *poly, *label, *psize, *polysize, *fog;
  MenuEnt *step, *fwd, *lumvar, *slum, *colorvar, *where[4];
  MenuEnt *viewall, *setscale, *speed, *fspeed, *every;
  MenuEnt *seedata, *box, *boxlevel[12];
  MenuEnt *annot;
  MenuEnt *tknob[3];
  MenuEnt *menu;
} ment;

struct boxleveler {
  struct stuff *st;
  int level;
};

void set_tknob( float val, MenuEnt *me, void *st );
void set_psize( float psize, MenuEnt *me, void *st );
void set_polysize( float psize, MenuEnt *me, void *st );
void set_alpha( float alpha, MenuEnt *me, void *st );
void set_point( int on, MenuEnt *me, void *st );
void set_poly( int on, MenuEnt *me, void *st );
void set_label( int on, MenuEnt *me, void *st );

void set_step( float time0, MenuEnt *me, void *st );
void set_fwd( int fwd, MenuEnt *me, void *st );
void set_slum( float slum, MenuEnt *me, void *st );
void set_lumvar( int which, MenuEnt *me, void *st );
void set_colorvar( int which, MenuEnt *me, void *st );
void set_seedata( int which, MenuEnt *me, void *st );
void set_speed( float speed, MenuEnt *me, void *st );
void set_fspeed( float fspeed, MenuEnt *me, void *st );
void set_every( float every, MenuEnt *me, void *st );
void set_scale( float logscale, MenuEnt *me, void *st );
void set_viewall( int all, MenuEnt *me, void *st );
void set_allboxes( int on, MenuEnt *me, void *st );
void set_boxlevel( int on, MenuEnt *me, void *bleveler );
void set_where( int unused, MenuEnt *me, void *st );
void set_menu( int on, MenuEnt *me, void *st );
#endif /*CAVEMENU*/

static int defcmap[] = {
  0x11eeee00,
  0x1106ee00, 0x120ea900, 0x1316ce00, 0x1520d500, 0x172ece00, 0x193fcb00,
  0x1c54b400, 0x206da400, 0x24889200, 0x2aa58400, 0x33c07600, 0x3ed96a00,
  0x4eed6100, 0x63fa5a00, 0x7efe5100, 0x99fb4c00, 0xafef4700, 0xc0dc4000,
  0xcbc33900, 0xd4a83000, 0xda8b2900, 0xdf702700, 0xe2572500, 0xe5411700,
  0xe82f1000, 0xea211500, 0xeb171400, 0xed0f1200, 0xee0aee00,
  0xffffff00,
};

int orientboxcolor = PACKRGBA( 0xff, 0xff, 0, 0xff );

void specks_read( struct stuff **stp, char *fname );
void specks_read_boxes( struct stuff *st, char *fname, int timestep );
int  specks_add_box( struct stuff *st, struct AMRbox *box, int timestep );
int  specks_purge( void *vstuff, int nbytes, void *aarena );
int  specks_count( struct specklist *head );
int  specks_gobox( struct stuff *st, int boxno, int argcrest, char *argvrest[] );
int specks_cookcment( struct stuff *st, int cment );
void specks_rgbremap( struct stuff *st );

void specks_reupdate( struct stuff *st, struct specklist *sl );
struct specklist **specks_find_annotation( struct stuff *, struct specklist **);
void specks_set_current_annotation( struct stuff *st, char *annotation );
void specks_add_annotation( struct stuff *st, char *annotation, int timestep );
void specks_timerange( struct stuff *st, double *tminp, double *tmaxp );

extern int getbool( char *str, int defval );
extern double getfloat( char *str, double defval );
extern int getfloats( float *v, int nfloats, int arg0, int argc, char **argv );
extern int specks_set_byvariable( struct stuff *st, char *str, int *val );

void strncpyt( char *dst, char *src, int dstsize ) {
  int len = strlen(src);
  if(len >= dstsize) len = dstsize-1;
  memcpy(dst, src, len);
  dst[len] = '\0';
} 

#ifdef sgi
static float defgamma = 1.0;
#else
static float defgamma = 2.5;
#endif

struct stuff *
specks_init( int argc, char *argv[] )
{
  int i;
  struct stuff *st = NewN( struct stuff, 1 );

  i = PACKRGBA(1, 0, 0, 0);
  if(*(char *)&i != 1) {
    msg("specks_init(): trouble: is WORDS_BIGENDIAN mis-set?  Giving up.");
    exit(1);
  }

  memset(st, 0, sizeof(*st));
  st->spacescale = 1.0;
  st->fog = 0;
  st->psize = 1;
  st->alpha = .5;
  st->gamma = defgamma;
  st->rgbgamma[0] = st->rgbgamma[1] = st->rgbgamma[2] = 1.0;
  st->rgbright[0] = st->rgbright[1] = st->rgbright[2] = 1.0;
  specks_rgbremap( st );
  st->alias = NULL;
  st->useme = 1;
  st->usepoly = 0;
  st->usepoint = 1;
  st->usetext = 1;
  st->usetextaxes = 1;
  st->usetextures = 1;
  st->useboxes = 1;
  st->usemeshes = 1;
  st->staticmeshes = NULL;
  st->mullions = 0;
  st->useellipsoids = 1;
  st->polysizevar = -1;
  st->polyarea = 0;
  st->polyorivar0 = -1;
  st->texturevar = -1;
  st->txscale = .5;
  st->boxlabels = 0;
  st->boxlabelscale = 1.0;
  st->boxlevelmask = ~0;	/* all levels on */
  st->boxaxes = 0;		/* boxes don't show orientation markers */
  st->goboxscale = 1.0;
  st->textsize = .05;
  st->npolygon = 11;
  st->subsample = 1;
  st->everycomp = 1;
  st->maxcomment = sizeof(st->sl->specks->title) - 1;
  st->dyn.enabled = 0;
  st->dyn.data = NULL;
  st->dyn.getspecks = NULL;
  st->dyn.draw = NULL;
  st->dyn.trange = NULL;
  st->dyn.help = NULL;
  st->dyn.ctlcmd = NULL;
  st->dyn.free = NULL;
  st->speckseq = 0;

  st->menudemandfps = 4.0;

  st->pfaint = .05;	/* params for "fast" point-drawing */
  st->plarge = 10;
  st->polymin = .5;	/* don't draw polygons if smaller (pixels) */
  st->polymax = 1e8;	/* don't allow polygons to get bigger than this (pixels) */
  st->polyfademax = 0;
  st->textmin = 2;	/* replace labels with line-segments if smaller (pixels) */
  st->ntextures = 0;
  st->textures = NULL;

  st->fade = F_SPHERICAL;
  st->fadeknee1 = 10.0;
  st->fadeknee2 = 1.0;
  st->knee2steep = 1.0;

  st->gscale = 1.;
  st->gtrans.x[0] = st->gtrans.x[1] = st->gtrans.x[2] = 0;
  st->objTo2w = Tidentity;

  st->ncmap = st->boxncmap = st->textncmap = COUNT(defcmap);
  st->cmap = NewN(struct cment, COUNT(defcmap));
  st->boxcmap = NewN(struct cment, COUNT(defcmap));
  st->textcmap = NewN(struct cment, COUNT(defcmap));
  for(i = 0; i < COUNT(defcmap); i++) {
    st->cmap[i].raw = st->boxcmap[i].raw = st->textcmap[i].raw
	= htonl(defcmap[i]);
    st->cmap[i].cooked = st->boxcmap[i].cooked = st->textcmap[i].cooked
	= specks_cookcment( st, st->cmap[i].raw );
  }
  /* Ensure that textcmap[0] is white by default */
  st->textcmap[0].raw = PACKRGBA( 170, 170, 170, 0 );
  st->textcmap[0].cooked = specks_cookcment( st, st->textcmap[0].raw );

  st->sizedby = 0;
  st->coloredby = 1;
  st->sizeseq = st->colorseq = st->threshseq = 0;
  st->trueradius = 0;
  st->sdbvars = shmstrdup( "mcr" );

  st->useemph = 0;
  selinit( &st->emphsel );
  st->emphfactor = 10;

  selinit( &st->threshsel );
  selinit( &st->seesel );

  st->ntimes = 0;
  st->ndata = 0;
  st->curtime = 0;
  st->curdata = 0;
  st->datatime = 0;
  st->usertrange = 0;
  st->utmin = -HUGE;
  st->utmax = HUGE;
  st->utwrap = 0.1;
  st->sl = NULL;

  st->boxes = NULL;
  st->boxlevels = 0;
  st->boxlinewidth = 0.75;

  st->depthsort = 0;

  st->clk = NewN(SClock, 1);
  clock_init(st->clk);
  clock_set_running(st->clk, 1);

  memset(st->anima, 0, sizeof(st->anima));
  memset(st->annot, 0, sizeof(st->annot));
  memset(st->datafile, 0, sizeof(st->datafile));
  memset(st->fname, 0, sizeof(st->fname));
  memset(st->meshes, 0, sizeof(st->meshes));

#if CAVE
  shmrecycler( specks_purge, st );
#endif

  for(i = 1; i < argc; i++)
    specks_read( &st, argv[i] );

#if CAVEMENU
  {
    char *menuopt;
    menu_preinit();

    pmenu = menu_create( .05, .95 );
    menu_setfont( pmenu, font_small );

    ment.annot = menu_addentry( pmenu, " ", NULL, NULL );
    menu_addentry( pmenu, " ", NULL, NULL );

    menuopt = getenv("PARTIMENU");
    if(menuopt == NULL) {
	menuopt = "label";
	st->hidemenu = 1;
    }

    if(strstr(menuopt, "survey")) {	/* SC99DEMO star-formation survey */
	for(i = 0; i < 3; i++) {
	    struct boxleveler *tb = NewN( struct boxleveler, 1 );
	    tb->st = st;
	    tb->level = i;
	    ment.tknob[i] = menu_addknob( pmenu, NULL, set_tknob, 0, 0,4, tb );
	}
    }
    menu_addentry( pmenu, " ", NULL, NULL );

    ment.psize = menu_addknob( pmenu, NULL, set_psize, st->psize, 0.1,20, st );
    ment.slum = menu_addknob( pmenu, NULL, set_slum,
		st->vdesc[st->curdata][st->sizedby].lum, .1, 10, st );
    ment.lumvar = menu_addtoggle( pmenu, NULL, set_lumvar, st->sizedby, MAXVAL, st );
    ment.colorvar = menu_addtoggle( pmenu, NULL, set_colorvar, st->coloredby, MAXVAL, st );
    ment.seedata = menu_addtoggle( pmenu, NULL, set_seedata, st->curdata, 2, st );
    if(strstr(menuopt, "label"))
    ment.label = menu_addtoggle( pmenu, NULL, set_label, st->usetext, 2, st );
    if(strstr(menuopt, "poly"))
    ment.poly = menu_addtoggle( pmenu, NULL, set_poly, st->usepoly, 2, st );
    menu_addentry( pmenu, " ", NULL, NULL );	/* spacer */

#ifndef STATIC_SPECKS
    ment.fspeed = menu_addknob( pmenu, NULL, set_fspeed, clock_speed(st->clk), 0, 5, st );
    ment.speed = menu_addknob( pmenu, NULL, set_speed, clock_speed(st->clk), .5, 5, st );
    ment.step = menu_addknob( pmenu, NULL, set_step, clock_time(st->clk), 0, 10, st );
    ment.fwd = menu_addtoggle( pmenu, NULL, set_fwd, clock_fwd(st->clk), 2, st );
#endif

#ifndef NO_WHERE
    ment.where[0] = menu_addentry( pmenu, "", NULL, NULL );
    ment.where[1] = menu_addentry( pmenu, "", NULL, NULL );
    ment.where[2] = menu_addentry( pmenu, "", NULL, NULL );
    ment.where[3] = menu_addentry( pmenu, "", NULL, NULL );
#endif

    ment.every = menu_addknob( pmenu, NULL, set_every, st->subsample, 1, 10, st );

    menu_addentry( pmenu, " ", NULL, NULL );

    ment.setscale = menu_addknob( pmenu, NULL, set_scale, 0., -4., 1., st );
    if(strstr(menuopt, "viewall"))
	ment.viewall = menu_addtoggle( pmenu, NULL, set_viewall, 0, 2, st );

    menu_addentry( pmenu, " ", NULL, NULL );

    ment.box = menu_addtoggle( pmenu, NULL, set_allboxes, st->useboxes, 3, st );
    menu_addentry( pmenu, " ", NULL, NULL );
    {
	struct boxleveler *blev = NewN( struct boxleveler, COUNT(ment.boxlevel) );
	for(i = 0; i < COUNT(ment.boxlevel); i++) {
	    blev[i].st = st;
	    blev[i].level = i;
	    ment.boxlevel[i] = menu_addtoggle( pmenu, NULL, set_boxlevel, 1, 2,
		    &blev[i] );
	}
    }

    ment.menu = menu_addtoggle( pmenu, "<Menu>", set_menu, 0, 2, st );
    menu_addentry( pmenu, " ", NULL, NULL );


    stubmenu = menu_create( .05, .15 );
    menu_addtoggle( stubmenu, "<Menu>", set_menu, 0, 2, st );
    menu_sethidden( stubmenu, 1 );
  }
#endif /*NOCAVE*/

  return st;
}

#if CAVEMENU
void specks_refresh_menu(struct stuff *st)
{
  int i;
  int empty = st->ntimes == 0 && st->sl == NULL;

#if MENU_IN_PPR
  menu_check( pmenu, pmenu->cavewall );
  menu_check( stubmenu, stubmenu->cavewall );

#endif
  menu_sethidden( pmenu, st->hidemenu || empty );
  menu_sethidden( stubmenu, st->hidemenu<=0 || empty );

  if(st->vdcmd[0] != '\0') {
    VIDI_queue_commandstr( st->vdcmd );
    st->vdcmd[0] = '\0';
  }
  set_psize( st->psize, ment.psize, st );
  set_lumvar( st->sizedby, ment.lumvar, st );
  set_colorvar( st->coloredby, ment.colorvar, st );
  set_slum( st->vdesc[st->curdata][st->sizedby].lum, ment.slum, st );
  set_every( st->subsample, ment.every, st );
  set_scale( ment.setscale->val, ment.setscale, st );
  if(ment.label) set_label( st->usetext, ment.label, st );
  if(ment.poly) set_poly( st->usepoly, ment.poly, st );
  if(ment.annot)
    menu_settitle( ment.annot, st->annotation ? st->annotation : " " );
  if(st->ntimes > 1) {
    menu_setknobrange( ment.step, 0, st->ntimes-1 );
    set_fwd( st->timefwd, ment.fwd, st );
    set_step( st->time0, ment.step, st );
    set_speed( clock_speed(st->clk), ment.speed, st );
  } else {
    menu_settitle( ment.fwd, "" );
    menu_settitle( ment.step, "" );
    menu_settitle( ment.speed, "" );
    if(ment.viewall)
      menu_settitle( ment.viewall, "" ); /* no need to "view peak", right? */
  }
  if(st->ndata > 1) {
    set_seedata( st->curdata, ment.seedata, st );
  } else {
    menu_settitle( ment.seedata, "" );
  }
  if(st->boxlevels > 0) {
    set_allboxes( st->useboxes, ment.box, st );
  } else {
    menu_settitle( ment.box, "" );
  }

  if(ment.tknob[0])	/* if SC99DEMO, i.e. if PARTIMENU includes "survey" */
    for(i = 0; i < 3; i++)
      set_tknob( ment.tknob[i]->val, ment.tknob[i], ment.tknob[i]->data );


#ifndef NO_WHERE
  for(i = 0; i < COUNT(ment.where) && ment.where[i] != NULL; i++)
    set_where( i, ment.where[i], st );
#endif

  /* Publicize our menu location so other apps -- Matt's vtk AMR code --
   * can avoid interacting if user's wand points there.
   */
  parti_menuwall = pmenu->cavewall;

  parti_menubox[0] = pmenu->x0 * pmenu->wallxpix;
  parti_menubox[1] = ((pmenu->y0 < pmenu->y1) ? pmenu->y0 : pmenu->y1)
			* pmenu->wallypix;

  parti_menubox[2] = pmenu->x1 * pmenu->wallxpix;
  parti_menubox[3] = ((pmenu->y0 > pmenu->y1) ? pmenu->y0 : pmenu->y1)
			* pmenu->wallypix;

}

void specks_evoke_menu( struct stuff *st ) {
  menu_evoke( pmenu );	/* evoke any callbacks */
}

#endif /*CAVEMENU*/

void specks_rethresh( struct stuff *st, struct specklist *sl, int by )
{
  int i;
  int curdata = st->curdata;
  int nel = sl->nspecks;
  float threshmin = st->thresh[0], threshmax = st->thresh[1];
  struct speck *p = sl->specks;
  SelMask *sel = sl->sel;
  SelOp threshseldest;
  int changed = 0;
  int min, max;

  sl->threshseq = st->threshseq;

  if(sl->text != NULL)	/* specklists with labels shouldn't be thresholded */
    return;

  if(curdata >= st->ndata)
    curdata = 0;

  min = (SMALLSPECKSIZE(by)>=sl->bytesperspeck) ? 0 : st->usethresh&P_THRESHMIN;
  max = (SMALLSPECKSIZE(by)>=sl->bytesperspeck) ? 0 : st->usethresh&P_THRESHMAX;
 

  threshseldest = st->threshsel;

  for(i = 0, p = sl->specks; i < nel; i++, p = NextSpeck( p, sl, 1 )) {
    SelMask was = sel[i];
    if((min&&p->val[by]<threshmin) || (max&&p->val[by]>threshmax)) {
	p->rgba |= THRESHBIT;
	SELUNSET( sel[i], &threshseldest );
    } else {
	p->rgba &= ~THRESHBIT;
	SELSET( sel[i], &threshseldest );
    }
    changed |= was ^ sel[i];
  }
  if(changed) {
    sl->selseq++;
    if(st->selseq < sl->selseq) st->selseq = sl->selseq;
  }
}

int specks_cookcment( struct stuff *st, int cment )
{
    unsigned char crgba[4];
    memcpy(crgba, &cment, 4);		/* XXX 64-bit bug? */
    return (cment & ~RGBWHITE) |
	   PACKRGBA(
		st->rgbmap[0][crgba[0]],
		st->rgbmap[1][crgba[1]],
		st->rgbmap[2][crgba[2]],
		0 );
}

void specks_rgbremap( struct stuff *st )
{
  int i, k;
  for(i = 0; i < 256; i++) {
    float t = (float)i / 256;
    float v = 255.99f * pow(t, 1/st->rgbgamma[0]);
    k = (int) (st->rgbright[0] * v);
    st->rgbmap[0][i] = (k <= 0) ? 0 : (k > 255) ? 255 : k;
    if(st->rgbgamma[0] != st->rgbgamma[1])
        v = 255.99f * pow(t, 1/st->rgbgamma[1]);
    k = (int) (st->rgbright[1] * v);
    st->rgbmap[1][i] = (k <= 0) ? 0 : (k > 255) ? 255 : k;
    if(st->rgbgamma[1] != st->rgbgamma[2])
        v = 255.99f * pow(t, 1/st->rgbgamma[2]);
    k = (int) (st->rgbright[2] * v);
    st->rgbmap[2][i] = (k <= 0) ? 0 : (k > 255) ? 255 : k;
  }
  /* remake colormap too */
  for(i = 0; i < st->ncmap; i++)
    st->cmap[i].cooked = specks_cookcment( st, st->cmap[i].raw );
  st->colorseq++;
}


void specks_recolor( struct stuff *st, struct specklist *sl, int by )
{
  struct valdesc *vd;
  int i;
  int curdata = st->curdata;
  int nel = sl->nspecks;
  struct speck *sp = sl->specks;
  float cmin, cmax, normal;
  int ncmap = st->ncmap;
  struct cment *cmap = st->cmap;
  int index;
  int rgb565;
  unsigned char (*rgbmap)[256];

  sl->coloredby = by;
  sl->colorseq = st->colorseq;

  if(sl->text != NULL)	/* specklists with labels shouldn't be recolored */
    return;

  if(curdata >= st->ndata)
    curdata = 0;

  if(by == CONSTVAL) {
    /* Hack -- color by given RGB value */
    char crgba[4];
    int r, g, b, rgba;
    vd = &st->vdesc[curdata][CONSTVAL];
    r = (vd->cmin<=0) ? 0 : vd->cmin>=1 ? 255 : (int)(255.99f * vd->cmin);
    g = (vd->cmax<=0) ? 0 : vd->cmax>=1 ? 255 : (int)(255.99f * vd->cmax);
    b = (vd->mean<=0) ? 0 : vd->mean>=1 ? 255 : (int)(255.99f * vd->mean);
    crgba[0] = st->rgbmap[0][r];
    crgba[1] = st->rgbmap[0][g];
    crgba[2] = st->rgbmap[0][b];
    crgba[3] = 0;
    rgba = *(int *)&crgba[0];	/* XXX 64-bit bug? */
    for(i = 0; i < nel; i++, sp = NextSpeck(sp, sl, 1)) {
	sp->rgba = rgba | (sp->rgba & EXTRABITS);
    }
    return; 
  }

  if(by >= MAXVAL || by < 0 || SMALLSPECKSIZE(by) > sl->bytesperspeck)
	by = 0;
	
  vd = &st->vdesc[curdata][by];

  if(vd->vncmap > 0) {
    ncmap = vd->vncmap;
    cmap = vd->vcmap;
  }


  cmin = vd->cmin, cmax = vd->cmax;
  if(cmin == cmax && cmin == 0 || (vd->call && !vd->cexact)) {
    vd->cmin = cmin = vd->min;
    vd->cmax = cmax = vd->max;
  }

  normal = (cmax != cmin && ncmap>2) ? (ncmap-2)/(cmax - cmin) : 0;
  rgb565 = !strcmp(vd->name, "rgb565") || !strcmp(vd->name, "colors565");

  /* cexact field means:
   *   0 (default): scale data range to cmap index 1..ncmap-2;
   *		    use 0 and ncmap-1 for low- and high- out-of-range values.
   *
   *   1 ("exact"): use data value as literal colormap index, 0..ncmap-1.
   */
  rgbmap = &st->rgbmap[0];
  for(i = 0; i < nel; i++, sp = NextSpeck(sp, sl, 1)) {
    if(rgb565) {
	index = sp->val[by];
	sp->rgba = PACKRGBA(
			rgbmap[0][(index&0xF800)>>(11-(8-5))],
			rgbmap[1][(index&0x07E0)>>(5-(8-6))],
			rgbmap[2][(index&0x1F)<<(8-5)],
			0 )
		  | (sp->rgba & EXTRABITS);
    } else {
	index = vd->cexact  ?   sp->val[by] + cmin
			    :  (sp->val[by] - cmin) * normal + 1;

	if(index < 0) index = 0;
	else if(index >= ncmap) index = ncmap-1;

	sp->rgba = cmap[index].cooked | (sp->rgba & EXTRABITS);
    }
  }
}

void specks_resize( struct stuff *st, struct specklist *sl, int by )
{
  int i;
  int nel = sl->nspecks;
  struct speck *sp = sl->specks;
  int curdata = st->curdata;
  struct valdesc *vd;
  float lmin, lmax, normal;

  sl->sizedby = by;
  sl->sizeseq = st->sizeseq;

  if(sl->text != NULL) /* specklists with labels shouldn't be resized */
    return;

  if(curdata < 0 || curdata >= st->ndata)
    curdata = 0;

  if(by == CONSTVAL) {
    vd = &st->vdesc[curdata][CONSTVAL];
    for(i = 0; i < nel; i++, sp = NextSpeck(sp, sl, 1)) {
	sp->size = vd->lmin;
	if(SELECTED(sl->sel[i], &st->emphsel)) {
	    sp->size *= st->emphfactor;
	}
    }
    return;
  }

  if(by < 0 || by >= MAXVAL)
    by = 0;

  vd = &st->vdesc[curdata][by];

  lmin = vd->lmin, lmax = vd->lmax;
  if(lmin == lmax && lmin == 0 || vd->lall) {
	vd->lmin = lmin = vd->min;
	vd->lmax = lmax = vd->max;
  }

  if(lmax == lmin)
    normal = 1;
  else
    normal = 1 / (lmax - lmin);

  if(st->useemph && st->emphsel.use != SEL_NONE) {
    for(i = 0; i < nel; i++, sp = NextSpeck(sp, sl, 1)) {
	sp->size = (sp->val[by] - lmin) * normal;
	if(SELECTED(sl->sel[i], &st->emphsel)) {
	    sp->size *= st->emphfactor;
	}
    }
  } else {
    /* normal case */
    for(i = 0; i < nel; i++, sp = NextSpeck(sp, sl, 1))
	sp->size = (sp->val[by] - lmin) * normal;
  }
}

void specks_datawait(struct stuff *st) {
#if USE_IEEEIO
    while(st->fetching && st->fetchpid > 0
	    && st->fetchtime == st->curtime
	    && st->fetchdata == st->curdata)
	usleep(50000);
#endif
}

#ifdef USE_IEEEIO

void specks_ieee_server( void *vst ) {
  struct stuff *st = (struct stuff *)vst;
  struct specklist *sl;

  prctl(PR_TERMCHILD);	/* Die when parent dies */
  /* Await a request */
  for(;;) {
    time_t then;
    while(st->fetching <= 0)
	sginap(5);
    then = time(NULL);
    sl = specks_ieee_read_timestep( st, st->subsample,
				st->fetchdata, st->fetchtime );
    if(then + 5 < time(NULL))
	msg("... got %x (%d particles) from %s", sl, specks_count(sl),
		st->fname[st->fetchdata][st->fetchtime]);
    if(st->curtime == st->fetchtime && st->curdata == st->fetchdata
			&& sl != NULL) {
	st->sl = sl;
	parti_redraw();
    }
    st->fetching = 0;
  }
}

#endif /*USE_IEEEIO*/

void specks_set_timebase( struct stuff *st, double timebase ) {
  clock_set_timebase( st->clk, timebase );
  /* parti_set_timebase( st, timebase ); */
}

void specks_set_speed( struct stuff *st, double newspeed ) {
  clock_set_speed( st->clk, newspeed );
  parti_set_speed( st, newspeed );
}

void specks_set_fspeed( struct stuff *st, double newspeed ) {
  /* ignore it! */
  /* or maybe use fspeed to specify finite clock resolution */
}

void specks_set_timestep( struct stuff *st )
{
  struct specklist *sl = st->sl;
  struct specklist **slp;
  double realtime, tmin, tmax;
  int timestep;

  st->used++;

  tmin = st->clk->tmin;
  tmax = st->clk->tmax;
  specks_timerange( st, &tmin, &tmax );
  if(st->usertrange) {
    /*if(tmin < st->utmin)*/ tmin = st->utmin;
    /*if(tmax > st->utmax)*/ tmax = st->utmax;
  }
  /* Only expand clock's existing time range */
  if(tmin > st->clk->tmin) tmin = st->clk->tmin;
  if(tmax < st->clk->tmax) tmax = st->clk->tmax;
  clock_set_range( st->clk, tmin, tmax, st->utwrap );
  realtime = clock_time( st->clk );
  timestep = realtime;

  if(st->dyn.enabled && st->dyn.getspecks) {
    struct specklist *sl;

    if(realtime == st->currealtime && st->sl != NULL && st->sl->used >= 0)
	return;
    sl = (*st->dyn.getspecks)(&st->dyn, st, realtime);
    if(sl == NULL)
	return;
    sl->used = 1;
    st->currealtime = realtime;
    st->curtime = timestep;

    slp = specks_timespecksptr( st, 0, 0 );
    st->sl = sl;
    /* Do we need this? */ /* *slp = sl; */ /* Guess not... */
    parti_set_timestep( st, realtime );
    return;
  }

  if(timestep >= st->ntimes) timestep = st->ntimes - 1;
  if(timestep < 0) timestep = 0;

  slp = specks_timespecksptr( st, st->curdata, timestep );
  if(*slp != NULL && (*slp)->subsampled > st->subsample) {
    specks_discard( st, slp );
  }

  sl = *slp;

#ifdef USE_IEEEIO

  if(sl == NULL && (st->fetching == 0 || st->datasync)
		&& st->curdata>=0 && st->curdata<st->ndata
		&& st->datafile[st->curdata] != NULL
		&& timestep>=0 && timestep<st->ntimes
		&& st->datafile[st->curdata][timestep] != NULL) {

    st->fetchdata = st->curdata;
    st->fetchtime = timestep;

    if(getenv("NO_SPROC") || st->datasync) {
	sl = specks_ieee_read_timestep( st, st->subsample,
                                st->fetchdata, st->fetchtime );
        msg("Got %x (%d particles) from %s  (d%d t%d)\n",
		sl, specks_count(sl), st->fname[st->fetchdata][st->fetchtime],
		st->fetchdata, st->fetchtime);

    } else {
	st->fetching = 1;

	if(st->fetchpid <= 0) {
	    st->fetchpid = sproc( specks_ieee_server, PR_SADDR|PR_SFDS, st );
	    if(st->fetchpid < 0)
		perror("sproc");
	}
    }
  }

  if(st->fetchpid > 0) {
    if(kill(st->fetchpid, 0) < 0) {
	perror("specks server vanished: kill -0");
	st->fetching = 0;
	st->fetchpid = 0;
    }
  }
#endif /*USE_IEEEIO*/

  if(st->skipblanktimes
	&& sl == NULL
	&& !(st->boxtimes>timestep && st->boxes[timestep]!=NULL)
	&& st->meshes[st->curdata][timestep] == NULL)
    return;

  st->sl = sl;   /* st->sl <= anima[][] */
  st->curtime = timestep;
  st->currealtime = timestep;
#if !CAVEMENU
  st->frame_time = st->curtime;	/* if non-CAVE, we have no frame-function */
#endif

  { struct specklist *asl = CURDATATIME(annot);
    specks_set_current_annotation( st, asl ? asl->text : NULL );
  }
  parti_set_timestep( st, timestep );

}

void specks_set_current_annotation( struct stuff *st, char *annotation )
{
  st->annotation = annotation;
#ifdef CAVEMENU
  if(ment.annot)
    menu_settitle( ment.annot, st->annotation ? st->annotation : "" );
#endif
}

void specks_add_annotation( struct stuff *st, char *annotation, int timestep )
{
  struct specklist *sl, **slp;
  int curtime = (timestep < 0) ? st->curtime : timestep;

  if(annotation == NULL) annotation = "";

  specks_timespecksptr( st, st->curdata, curtime );
  if(curtime >= st->ntimes) curtime = 0;
  slp = &st->annot[st->curdata][curtime];	/* safe -- no need for CURDATATIME() check */
  if((sl = *slp) == NULL) {
    sl = *slp = NewN( struct specklist, 1 );
    memset(*slp, 0, sizeof(**slp));
    sl->speckseq = ++st->speckseq;
  } else if(sl->text) {
    Free(sl->text);
  }
  sl->text = NewN( char, strlen(annotation)+1 );
  strcpy( sl->text, annotation );
}


#if !WORDS_BIGENDIAN
void starswap(db_star *st) {
  int i, *wp;
  /* byte-swap x,y,z, dx,dy,dz, magnitude,radius,opacity fields (32-bit float),
   * 		num (32-bit int),
   *		color (16-bit short).
   * group and type fields shouldn't need swapping,
   * assuming the compiler packs bytes into a word in increasing
   * address order.  Seems safe.
   */
  for(i = 0, wp = (int *)st; i < 10; i++)
    wp[i] = htonl(wp[i]);
  st->color = htons(st->color);
}
#endif /*!WORDS_BIGENDIAN*/

void specks_read_sdb( struct stuff *st, char *sdbfname, int timestep )
{
  FILE *inf = fopen(sdbfname, "rb");
  long flen;
  int nspecks, i;
  float min[MAXVAL], max[MAXVAL], sum[MAXVAL];
  struct specklist *sl, **slp;
  register struct speck *sp;
  int dfltvars = (strcmp(st->sdbvars, "mcr") == 0);
  int nvars = strlen(st->sdbvars);

  if(inf == NULL) {
    msg("sdb: %s: cannot open: %s", sdbfname, strerror(errno));
    return;
  }
  /* Just measure file size */
  errno = 0;
  fseek(inf, 0, SEEK_END);
  flen = ftell(inf);
  if(flen == -1 || flen == 0) {
    msg("sdb: %s: can't measure length of file: %s", sdbfname, strerror(errno));

    return;
  }
  nspecks = (flen / sizeof(db_star));

  if(nspecks <= 0) {
    msg("sdb: %s: ignoring empty sdb file", sdbfname);
    return;
  }
  
  sl = NewN(struct specklist, 1);
  memset(sl, 0, sizeof(*sl));
  sl->speckseq = ++st->speckseq;
  
  sl->bytesperspeck = SMALLSPECKSIZE( nvars );
  if(nvars > MAXVAL) nvars = MAXVAL;

  sl->scaledby = st->spacescale;
  sp = NewNSpeck(sl, nspecks);
  sl->specks = sp;
  sl->sel = NewN( SelMask, nspecks );
  sl->nsel = nspecks;
  memset(sl->sel, 0, nspecks*sizeof(SelMask));

  fseek(inf, 0, SEEK_SET);
  for(i = 0; i < nspecks; i++, sp = NextSpeck(sp, sl, 1)) {
    db_star star;
    char *cp;
    int k;
    float *vp;
    if(fread(&star, sizeof(star), 1, inf) <= 0)
	break;
#if !WORDS_BIGENDIAN
    starswap(&star);
#endif
    sp->p.x[0] = star.x * sl->scaledby;
    sp->p.x[1] = star.y * sl->scaledby;
    sp->p.x[2] = star.z * sl->scaledby;
    if(dfltvars) {
	sp->val[0] = exp((-18-star.magnitude)*.921/*log(100)/5*/);
	sp->val[1] = star.color;
	sp->val[2] = star.radius;
    } else {
	for(vp = &sp->val[0], cp = st->sdbvars; *cp; cp++, vp++) {
	    switch(*cp) {
	    case 'm': *vp = exp((-18-star.magnitude)*.921/*log(100)/5*/); break;
	    case 'M': *vp = star.magnitude; break;
	    case 'c': *vp = star.color; break;
	    case 'r': *vp = star.radius; break;
	    case 'o': *vp = star.opacity; break;
	    case 'g': *vp = star.group; break;
	    case 't': *vp = star.type; break;
	    case 'x': *vp = star.dx; break;
	    case 'y': *vp = star.dy; break;
	    case 'z': *vp = star.dz; break;
	    case 'S': *vp = sqrt(star.dx*star.dx + star.dy*star.dy + star.dz*star.dz); break;
	    case 'n': *vp = star.num; break;
	    default: *vp = 1; break;
	    }
	}
    }

    if(i == 0) {
	for(k = 0; k < nvars; k++)
	    sum[k] = min[k] = max[k] = sp->val[k];
    } else {
	for(k = 0; k < nvars; k++) {
	    if(min[k] > sp->val[k]) min[k] = sp->val[k];
	    else if(max[k] < sp->val[k]) max[k] = sp->val[k];
	    sum[k] += sp->val[k];
	}
    }
  }
  sl->nspecks = i;
  sl->sizedby = 0;
  sl->coloredby = 1;

  /* Update statistics */
  if(sl->nspecks > 0) {
    struct valdesc *vdp = &st->vdesc[st->curdata][0];
    for(i = 0; i < nvars; i++, vdp++) {
	if(vdp->min > min[i]) vdp->min = min[i];
	if(vdp->max < max[i]) vdp->max = max[i];
	vdp->nsamples += sl->nspecks;
	vdp->sum += sum[i];
	vdp->mean = vdp->sum / vdp->nsamples;

	if(vdp->name[0] == '\0') {
	    char *name = "unk";
	    switch(st->sdbvars[i]) {
	    case 'm': name = "lumsdb"; break;
	    case 'M': name = "magsdb"; break;
	    case 'c': name = vdp->max > 16384 ? "rgb565" : "colorsdb";
		      vdp->cexact = 1;
		      break;
	    case 'r': name = "radius"; break;
	    case 'o': name = "opacity"; break;
	    case 'g': name = "group"; break;
	    case 't': name = "type"; break;
	    case 'x': name = "dx"; break;
	    case 'y': name = "dy"; break;
	    case 'z': name = "dz"; break;
	    case 'S': name = "speed"; break;
	    case 'n': name = "number"; break;
	    }
	    strcpy(vdp->name, name);
	}
    }
    specks_recolor( st, sl, st->coloredby );
    specks_resize( st, sl, st->sizedby );
  }

  /* Add to running list */
  slp = specks_timespecksptr( st, st->curdata, timestep );
  sl->next = *slp;
  *slp = sl;

  fclose(inf);
    
}

void specks_timerange( struct stuff *st, double *tminp, double *tmaxp )
{
  if(st->dyn.enabled && st->dyn.trange &&
	(*st->dyn.trange)(&st->dyn, st, tminp, tmaxp)) {
    /* OK */
  } else {
    *tminp = 0;
    *tmaxp = (st->ntimes == 0) ? 0 : st->ntimes - 1;
  }
}

int specks_get_datastep( struct stuff *st )
{
  return st->curtime;
}

double specks_get_realtime( struct stuff *st )
{
  return st->currealtime;
}

void set_interest_point( Point *p )
{
#if CAVEMENU
  char cmd[80];
  sprintf(cmd, "interest %g %g %g", p->x[0], p->x[1], p->x[2]);
  VIDI_queue_commandstr( cmd );
#else

  parti_center( p );
#endif
}

/* Only specks_timespecksptr() extends the spans of time or datasets */
struct specklist **
specks_timespecksptr( struct stuff *st, int dataset, int timestep )
{
  int d, needroom;
  struct specklist **na, **nan;
  void **ndf;
  char **nfn;
  struct mesh **nmesh;

  if((timestep >= st->ntimes || dataset >= st->ndata)) {
    needroom = st->timeroom;
    if(timestep >= st->timeroom)
	needroom = 2*timestep + 15;

    for(d = 0; d < st->ndata || (dataset < MAXFILES && d <= dataset); d++) {

	if(needroom == st->timeroom && d < st->ndata)
	    continue;

	na = NewN( struct specklist *, needroom );
	nan = NewN( struct specklist *, needroom );
	ndf = NewN( void *, needroom );
	nfn = NewN( char *, needroom );
	nmesh = NewN( struct mesh *, needroom );
	memset(na, 0, needroom * sizeof(*na));
	memset(nan, 0, needroom * sizeof(*nan));
	memset(ndf, 0, needroom * sizeof(*ndf));
	memset(nfn, 0, needroom * sizeof(*nfn));
	memset(nmesh, 0, needroom * sizeof(*nmesh));
	if(d < st->ndata && st->anima[d])
	    memcpy( na, st->anima[d], st->ntimes * sizeof(*na) );

	if(d < st->ndata && st->annot[d])
	    memcpy( nan, st->annot[d], st->ntimes * sizeof(*nan) );

	if(d < st->ndata && st->datafile[d])
	    memcpy( ndf, st->datafile[d], st->ntimes * sizeof(*ndf) );

	if(d < st->ndata && st->fname[d])
	    memcpy( nfn, st->fname[d], st->ntimes * sizeof(*nfn) );

	if(d < st->ndata && st->meshes[d])
	    memcpy( nmesh, st->meshes[d], st->ntimes * sizeof(*nfn) );

	/* Don't free old pointers, just in case they're in use. */
	st->anima[d] = na;
	st->annot[d] = nan;
	st->datafile[d] = ndf;
	st->fname[d] = nfn;
	st->meshes[d] = nmesh;
    }
    st->timeroom = needroom;

    if(timestep >= st->ntimes)
	st->ntimes = timestep + 1;
    if(dataset >= st->ndata && dataset < MAXFILES)
	st->ndata = dataset + 1;
  }

  return (timestep >= 0 && timestep < st->ntimes &&
			dataset >= 0 && dataset < st->ndata)
	? &st->anima[dataset][timestep] : NULL;
}

struct specklist *
specks_timespecks( struct stuff *st, int dataset, int timestep )
{
  return (timestep >= 0 && timestep < st->ntimes &&
			dataset >= 0 && dataset < st->ndata)
	? st->anima[dataset][timestep] : NULL;
}

void specks_reupdate( struct stuff *st, struct specklist *sl )
{
  struct specklist *tsl;

  if(sl != NULL && sl->threshseq != st->threshseq) {
    for(tsl = sl; tsl != NULL; tsl = tsl->next)
	specks_rethresh( st, tsl, st->threshvar );
  }

  if(sl != NULL && sl->colorseq != st->colorseq) {
    for(tsl = sl; tsl != NULL; tsl = tsl->next)
	specks_recolor( st, tsl, st->coloredby );
  }

  if(sl != NULL && sl->sizeseq != st->sizeseq) {
    for(tsl = sl; tsl != NULL; tsl = tsl->next)
	specks_resize( st, tsl, st->sizedby );
  }
}

static int specks_nonempty_timestep( struct stuff *st, int timestep ) {
  
  if(specks_timespecks( st, st->curdata, timestep ) != NULL)
    return 1;

  if((st->datafile[st->curdata] != NULL &&
		st->datafile[st->curdata][timestep] != NULL))
    return 1;

  if(timestep >= 0 && timestep < st->ntimes &&
		st->meshes[st->curdata][timestep] != NULL)
    return 1;

  if(timestep >= 0 && timestep < st->boxtimes &&
		st->boxes[timestep] != NULL)
    return 1;

  return 0;
}


void specks_set_time( struct stuff *st, double newtime )
{

  clock_set_time( st->clk, newtime );
  specks_set_timestep( st );
  if(st->sl == NULL && st->ntimes > 1 && st->clk->parent == NULL) {
    /* Skip blank time-slots -- keep incrementing until either:
     *  - we find a time-slot that has (or could have) some data, or
     *  - we've run through all time-steps (avoid infinite loops!).
     */
    int nudges, ts;
    if(st->skipblanktimes) {
	for(nudges = 0; nudges < st->ntimes; nudges++) {
	    ts = (st->curtime + nudges) % st->ntimes;
	    if( specks_nonempty_timestep( st, ts ) )
		break;
	}
    }
    clock_set_time( st->clk, ts );
    specks_set_timestep( st );
  }
  /* st->playnext = now + (st->fspeed != 0 ? 1/st->fspeed : 0); */

#if CAVE
  if(ment.tknob[0] == NULL) {	/* if not SC99DEMO */
    static Point lastinterest;
    struct specklist *sl = st->sl;
    if(sl != NULL && memcmp(&sl->interest, &lastinterest, sizeof(Point))
	    && (sl->interest.x[0]!=0 || sl->interest.x[1]!=0
					    || sl->interest.x[2]!=0)) {
	char str[128];
	float scale = .004;
	lastinterest = sl->interest;
	set_interest_point( &lastinterest );
	sprintf(str, "\002setjump peak %g %g %g 0 0 0 %g",
	    lastinterest.x[0], lastinterest.x[1]-scale*5, lastinterest.x[2] + scale*5,
	    scale);
	VIDI_queue_commandstr( str );
    }
  }
#endif

  specks_reupdate( st, st->sl );

#if CAVE
  specks_refresh_menu( st );
#endif
}

static int specks_freenow( struct specklist **slp, int maxage )
{
  struct specklist *sl, **sprev;
  int any = 0;

  for(sprev = slp; (sl = *sprev) != NULL && sl->used <= maxage; ) {
    if(sl->used <= maxage) {
	*sprev = sl->freelink;
	if(sl->specks != NULL)
	    Free(sl->specks);
	Free(sl);
	any++;
    } else {
	/* too recent - might still be in use */
	sprev = &sl->freelink;
    }
  }
  return any;
}

void specks_discard( struct stuff *st, struct specklist **slp )
{
  struct specklist *sl, *slnext;

  for(sl = *slp; sl != NULL; sl = slnext) {
    slnext = sl->next;
    sl->freelink = st->scrap;
    st->scrap = sl;
  }
  *slp = NULL;
}

int specks_purge( void *vst, int nbytes, void *aarena )
{
  struct stuff *st = (struct stuff *)vst;
  int oldused = st->used;
  int oldtime = -1, oldds = -1;
  int t, ds;
  struct specklist *sl;

#ifdef sgi
  static int first = 1;
  struct mallinfo mi;
  mi = amallinfo( aarena );

  if(first) {
    first = 0;
    msg("Purging %dKbyte shmem arena (currently %dK used in %d blks, %dK free)\n",
	mi.arena>>10, mi.uordblks>>10, mi.ordblks, mi.fordblks>>10);
  }
#endif

  /* Free any known scrap first */

#define OLD_ENOUGH  4

  if(specks_freenow( &st->scrap, st->used - OLD_ENOUGH ) > 0)
    return 1;

  for(t = 0; t < st->ntimes; t++) {
    if(t == st->curtime) continue;
    for(ds = 0; ds < st->ndata; ds++) {
	sl = st->anima[ds][t];
	if(sl != NULL && sl != st->sl && sl->used < oldused) {
	    oldused = st->used;
	    oldtime = t;
	    oldds = ds;
	}
    }
  }
  if(oldtime >= 0) {
    specks_discard( st, &st->anima[oldds][oldtime] );
    specks_freenow( &st->scrap, oldtime );
    return 1;	/* We freed something, so try allocating again */
  } else {
    msg("Ran out of shmem, couldn't find anything more to purge\n");
#ifdef sgi
    msg("%dKbyte shmem arena (currently %dK used in %d blks, %dK free)\n",
	mi.arena>>10, mi.uordblks>>10, mi.ordblks, mi.fordblks>>10);

#endif
    return 0;	/* No progress made -- give up */
  }
}


/*
 * Just stash these values in our frame function so they won't change visibly
 * during a frame.  Each frame function will do this; we'll just hope that
 * they don't change as the various cave-wall processes start.
 */
void specks_current_frame( struct stuff *st, struct specklist *sl )
{
  st->frame_sl = sl;
  st->frame_time = st->curtime;
  st->frame_data = st->curdata;
  st->frame_annotation = st->annotation;
  specks_reupdate( st, sl );
}

#define	MAXXYFAN 16

extern void specks_draw_boxes( struct stuff *st, struct AMRbox *boxes, int levelmask, Matrix Ttext, int oriented );

extern void specks_draw_mesh( struct stuff *st, struct mesh *m, int *texturing );
extern void specks_draw_ellipsoid( struct stuff *st, struct ellipsoid *e );

struct cpoint {
    int rgba;
    Point p;
};

void dumpcpoints( struct cpoint *cp, int n )
{
    while(--n >= 0) {
	glColor4ubv( (GLubyte *)&cp->rgba );
	glVertex3fv( &cp->p.x[0] );
	cp++;
    }
}

static Point depth_fwd;
static float depth_d;

struct order {
  float z;
  struct speck *sp;
  struct specklist *sl;
};

static int depthcmp( const void *a, const void *b )
{
  return ((struct order *)a)->z < ((struct order *)b)->z ?    1
	: ((struct order *)a)->z > ((struct order *)b)->z ? -1 : 0;
}

static int additive_blend;

void sortedpolys( struct stuff *st, struct specklist *slhead, Matrix *Tc2wp, float radperpix, float polysize )
{
  struct speck *sp;
  struct order *op, *obase;
  int i, k, total, skip;
  struct specklist *sl;
  int usethresh = st->usethresh&P_USETHRESH ? THRESHBIT : 0;
  int bps = 0;
  int prevrgba = -1;
  int usearea = st->polyarea;
  int sizevar = st->polysizevar;
  int polyorivar = st->polyorivar0;
  int texturevar = st->texturevar;
  int txno;
  int texturing = -1;
  float polyminrad = st->polymin * radperpix;
  float polymaxrad = st->polymax * radperpix;
  float mins2d = polyminrad * polyminrad;
  int rgba;
  int alpha = st->alpha * 255;
  int nfan = st->npolygon<MAXXYFAN ? st->npolygon : MAXXYFAN;
  float xyfan[MAXXYFAN][2];
  Point sfan[MAXXYFAN];
  Matrix Tc2w = *Tc2wp;
  float scl = vlength( (Point *)&Tc2w.m[0*4+0] );
  float fanscale;
  Texture *wanttx;
  int additive = additive_blend;
  int wantblend = additive;

  int useclip = (st->clipbox.level != 0);
  Point clipp0 = st->clipbox.p0;
  Point clipp1 = st->clipbox.p1;

  for(total = 0, sl = slhead; sl != NULL; sl = sl->next) {
    if(sl->text != NULL || sl->nspecks == 0 || sl->special != SPECKS)
	continue;
    skip = st->subsample;
    if(sl->subsampled != 0)	/* if already subsampled */
	skip /= sl->subsampled;
    if(skip <= 0) skip = 1;

    if(bps < sl->bytesperspeck) bps = sl->bytesperspeck;
    if(usethresh) {
	for(i=sl->nspecks, sp=sl->specks; i>0; i-=skip, sp=NextSpeck(sp,sl,skip)) {
	    if((usethresh & sp->rgba) == 0)
		total++;
	}
    } else {
	total += sl->nspecks / skip;
    }
  }

  obase = op = (struct order *)malloc( (total+1) * sizeof(struct order) );
  for(sl = slhead; sl != NULL; sl = sl->next) {
    if(sl->text != NULL || sl->nspecks == 0 || sl->special != SPECKS)
	continue;
    skip = st->subsample;
    if(sl->subsampled != 0)	/* if already subsampled */
	skip /= sl->subsampled;
    if(skip <= 0) skip = 1;
    for(i=sl->nspecks, sp=sl->specks; i > 0; i-=skip, sp=NextSpeck(sp,sl,skip)) {
	float dist;
	if(usethresh & sp->rgba)
	    continue;
	dist = VDOT( &sp->p, &depth_fwd ) + depth_d;
	if(dist < 0)
	    continue;
	if(useclip &&
	  (sp->p.x[0] < clipp0.x[0] ||
	   sp->p.x[0] > clipp1.x[0] ||
	   sp->p.x[1] < clipp0.x[1] ||
	   sp->p.x[1] > clipp1.x[1] ||
	   sp->p.x[2] < clipp0.x[2] ||
	   sp->p.x[2] > clipp1.x[2]))
	    continue;
	op->z = dist;
	op->sp = sp;
	op->sl = sl;
	op++;
    }
  }

  total = op - obase;
  qsort( obase, total, sizeof(*obase), depthcmp );

  prevrgba = 0;

  /* Build prototype fan -- unit disk in screen plane */
  /* Scale the fan big enough that the unit disk is inscribed in our polygon */
  fanscale = 1 / (scl * cos(M_PI/nfan));
  for(i = 0; i < nfan; i++) {
    float theta = 2*M_PI*(i+0.5f)/nfan;
    xyfan[i][0] = cos(theta);
    xyfan[i][1] = sin(theta);
    vcomb( &sfan[i], xyfan[i][0] * fanscale, (Point *)&Tc2w.m[0*4+0],
		     xyfan[i][1] * fanscale, (Point *)&Tc2w.m[1*4+0] );
  }

  if(st->usetextures == 0 || SMALLSPECKSIZE(texturevar) > bps)
    texturevar = -1;
  if(SMALLSPECKSIZE(polyorivar) > bps)
    polyorivar = -1;

  for(i = 0, op = obase; i < total; i++, op++) {
    float dist, size;

    sp = op->sp;
    dist = op->z;
    size = sp->val[sizevar] * polysize;
    if(usearea) {
	if(size < dist * dist * mins2d)
	    continue;
	size = sqrtf(size);
    } else {
	if(size < dist * polyminrad)
	    continue;
    } 
    if(size > dist * polymaxrad)
	size = dist * polymaxrad;

    rgba = sp->rgba & RGBBITS;
    if(rgba != prevrgba) {
	prevrgba = rgba;
	rgba = RGBALPHA( prevrgba, alpha );
	glColor4ubv( (GLubyte *)&rgba );
    }

    if(texturevar >= 0 &&
	    (txno = sp->val[texturevar]) >= 0 &&
	    txno < st->ntextures &&
	    (wanttx = st->textures[txno]) != NULL) {

	txbind( wanttx, &texturing );
	wantblend = (wanttx->flags & TXF_ADD) ? 1 : additive_blend;
    } else if(texturing) {
	glDisable( GL_TEXTURE_2D );
	texturing = 0;
    }

    if(wantblend != additive) {
	additive = wantblend;
	glBlendFunc( GL_SRC_ALPHA, additive ? GL_ONE : GL_ONE_MINUS_SRC_ALPHA );
    }

    if(polyorivar >= 0 && sp->val[polyorivar] < 9) {
	float *xv = &sp->val[polyorivar];
	float *yv = &sp->val[polyorivar+3];

	glBegin( GL_TRIANGLE_FAN );
	if(texturing) {
	    for(k = 0; k < nfan; k++) {
		glTexCoord2fv( &xyfan[k][0] );
		glVertex3f(
		    sp->p.x[0] + size*(xyfan[k][0]*xv[0] + xyfan[k][1]*yv[0]),
		    sp->p.x[1] + size*(xyfan[k][0]*xv[1] + xyfan[k][1]*yv[1]),
		    sp->p.x[2] + size*(xyfan[k][0]*xv[2] + xyfan[k][1]*yv[2]));
	    }
	} else {
	    for(k = 0; k < nfan; k++) {
		glVertex3f(
		    sp->p.x[0] + size*(xyfan[k][0]*xv[0] + xyfan[k][1]*yv[0]),
		    sp->p.x[1] + size*(xyfan[k][0]*xv[1] + xyfan[k][1]*yv[1]),
		    sp->p.x[2] + size*(xyfan[k][0]*xv[2] + xyfan[k][1]*yv[2]));
	    }
	}
	glEnd();

    } else {
	glBegin( GL_TRIANGLE_FAN );
	if(texturing) {
	    for(k = 0; k < nfan; k++) {
		glTexCoord2fv( &xyfan[k][0] );
		glVertex3f(
		    sp->p.x[0] + size*sfan[k].x[0],
		    sp->p.x[1] + size*sfan[k].x[1],
		    sp->p.x[2] + size*sfan[k].x[2] );
	    }
	} else {
	    for(k = 0; k < nfan; k++) {
		glVertex3f(
		    sp->p.x[0] + size*sfan[k].x[0],
		    sp->p.x[1] + size*sfan[k].x[1],
		    sp->p.x[2] + size*sfan[k].x[2] );
	    }
	}
	glEnd();
    }
  }
  free(obase);
  if(texturing > 0)
    txbind( NULL, NULL );
}
  
  

void drawspecks( struct stuff *st )
{
  int i, slno, k;
  int rgba, alpha, prevrgba = 0;
  float prevsize = 0;
  struct specklist *sl, *slhead;
  register struct speck *p;
  Matrix Tw2c, Tc2w, Ttext, Tproj, Ttemp;
  static Point zero = {0,0,0};
  int xywh[4];
  float radperpix;
  Point tp, fan[MAXXYFAN];
  Point eyepoint;
  Point fwd;
  float fwdd;
  float tscale, scl, fanscale;
  int skip;
  static int nxyfan = 0;
  static float xyfan[MAXXYFAN][2];
  static unsigned char randskip[256];
  int fast = st->fast;
  int inpick = st->inpick;
  /*int usethresh = st->usethresh&P_USETHRESH ? THRESHBIT : 0;*/
#if USE_PTRACK
  int useptrack = (getenv("PTRACK") != NULL);
#endif
  SelOp seesel = st->seesel;
  float polyminrad, polymaxrad;
  int useclip = (st->clipbox.level != 0);
  Point clipp0 = st->clipbox.p0;
  Point clipp1 = st->clipbox.p1;

  float plum = st->psize;

  float knee1dist2 = st->fadeknee1 * st->fadeknee1;
  float knee2dist2 = st->fadeknee2 * st->fadeknee2;
  float orthodist2 = st->fadeknee2 * st->fadeknee2;
  float steep2knee2 = st->knee2steep * st->knee2steep / knee2dist2;
  float faderball2 = 1 / (st->fadeknee2 * st->fadeknee2);
  Point fadecen = st->fadecen;
  enum FadeType fademodel = st->fade;

  if(!st->useme)
    return;

  switch(fademodel) {
  case F_CONSTANT:
	if(orthodist2 <= 0)
	    orthodist2 = 1;
	break;
  case F_KNEE12:
	if(st->fadeknee1 >= st->fadeknee2 || st->fadeknee1 <= 0)
	    fademodel = F_KNEE2;	/* and fall into... */
  case F_KNEE2:
	if(st->fadeknee2 <= 0)
	    fademodel = F_SPHERICAL;
	break;
  case F_LREGION:
	if(st->fadeknee2 <= 0) faderball2 = 1;
	break;
  }


  { float r=0,g=0,b=0;	/* Ugh. Allow background to be non-black */
    sscanf(parti_bgcolor(NULL), "%f%f%f", &r,&g,&b);
    additive_blend = (r+g+b == 0);
  }

  if(st->clipbox.level > 0) {
    GLdouble plane[4];

    if(st->clipbox.level == 1) {
	struct AMRbox b[2];
	b[0] = st->clipbox;
	b[0].level = 0;
	b[1].level = -1;
	specks_draw_boxes( st, b, ~0, Tidentity, 1 );
    }
    for(i = 0; i < 3; i++) {
	plane[0] = plane[1] = plane[2] = 0;
	plane[i] = 1;
	plane[3] = -st->clipbox.p0.x[i];
	glClipPlane( GL_CLIP_PLANE0 + i, plane );
	glEnable( GL_CLIP_PLANE0 + i );
	plane[i] = -1;
	plane[3] = st->clipbox.p1.x[i];
	glClipPlane( GL_CLIP_PLANE0 + 3 + i, plane );
	glEnable( GL_CLIP_PLANE0 + 3 + i );
    }
  }


  if(nxyfan != st->npolygon && st->npolygon > 0) {
    if(st->npolygon > MAXXYFAN) st->npolygon = MAXXYFAN;
    nxyfan = st->npolygon;
    fanscale = 1 / cos(M_PI/nxyfan);
    for(i = 0; i < nxyfan; i++) {
	float th = (i+.5f)*2*M_PI / nxyfan;
	xyfan[i][0] = cos(th) * fanscale;
	xyfan[i][1] = sin(th) * fanscale;
    }
    srandom(11);
    for(i = 0; i < 256; i++)
	randskip[i] = random() & 0xFF;
  }


  alpha = st->alpha * 255;
  rgba = RGBALPHA( RGBWHITE, alpha );	/* BIG-ENDIAN (1,1,1,alpha) */

  /* Find displacements which lie in the screen plane, for making
   * billboard-style polygonal patches, and for text.
   */
  glGetFloatv( GL_MODELVIEW_MATRIX, Tw2c.m );
  eucinv( &Tc2w, &Tw2c );
  scl = vlength( (Point *)&Tw2c.m[0] );
  for(i = 0; i < nxyfan; i++) {
    tp.x[0] = scl*st->polysize*xyfan[i][0];
    tp.x[1] = scl*st->polysize*xyfan[i][1];
    tp.x[2] = 0;
    vtfmvector( &fan[i], &tp, &Tc2w );
  }

  /* Find projection matrix and screen (well, viewport) size,
   * so we can convert angular sizes to screen (pixel) sizes,
   * in radians per pixel.
   */
  glGetFloatv( GL_PROJECTION_MATRIX, Tproj.m );
  glGetIntegerv( GL_VIEWPORT, xywh );
  radperpix = 1 / (.5*xywh[2] * Tproj.m[0*4+0]);

  /* Construct a "forward" vector in object coords too, for measuring
   * distance from camera plane.  Note camera looks toward its -Z axis not +Z!
   */
  tp.x[0] = 0, tp.x[1] = 0, tp.x[2] = -1;
  vtfmvector( &fwd, &tp, &Tc2w );
  vunit( &fwd, &fwd );
  /*
   * Actually we want a plane equation, whose value is zero in the
   * eye plane.  Camera-space distance from camera plane = 
   *		vdot( &objectpoint, &fwd ) + fwdd.
   */
  vtfmpoint( &eyepoint, &zero, &Tc2w );
  fwdd = -vdot( &eyepoint, &fwd );

  tscale = scl * st->textsize;
  mcopy( &Ttemp, &Tidentity );
  Ttemp.m[0*4+0] = Ttemp.m[1*4+1] = Ttemp.m[2*4+2] = tscale;
  mmmul( &Ttext, &Ttemp, &Tc2w );
  vsettranslation( &Ttext, &zero );

  glDisable( GL_LIGHTING );

  /* Draw any boxes (even if we have no specks) for this timestep */
  if(st->useboxes && st->boxlevelmask != 0
	&& st->frame_time >= 0 && st->frame_time < st->boxtimes
	&& st->boxes[st->frame_time] != NULL) {
    specks_draw_boxes( st, st->boxes[st->frame_time], st->boxlevelmask, Ttext, 0 );
  }
  if(st->useboxes && st->staticboxes != NULL) {
    specks_draw_boxes( st, st->staticboxes, st->boxlevelmask, Ttext, st->boxaxes );
  }

  slhead = st->frame_sl;	/* st->sl as snapped by specks_ffn() */
  if(slhead == NULL)
    slhead = st->sl;		/* maybe there is no specks_ffn() */

  if(st->dyn.enabled && st->dyn.draw)
    (*st->dyn.draw)( &st->dyn, st, slhead, &Tc2w, radperpix );

  skip = st->subsample;
  if(slhead && slhead->subsampled != 0)	/* if already subsampled */
    skip /= slhead->subsampled;
  if(skip == 0) skip = 1;

  for(sl = slhead; sl != NULL; sl = sl->next)
    sl->used = st->used;

  if((unsigned int)st->sizedby <= MAXVAL
			&& (unsigned int)st->curdata < MAXFILES
			&& st->vdesc[st->curdata][st->sizedby].lum != 0) {
	plum *= st->vdesc[st->curdata][st->sizedby].lum;
  }
  if(st->subsample > 0 && st->everycomp)
      plum *= st->subsample;	/* Compensate for "every" subsampling */


  if(st->alpha >= 1) {
    glDisable(GL_BLEND);
    glEnable(GL_DEPTH_TEST);
  } else {
    glEnable(GL_BLEND);
    glBlendFunc( GL_SRC_ALPHA, additive_blend ? GL_ONE : GL_ONE_MINUS_SRC_ALPHA );
    glEnable(GL_DEPTH_TEST);
    glDepthMask( GL_FALSE );
  }

  if(inpick) glLoadName(0);
  

  if(st->usepoly && st->polysize > 0) {
    int texturing = 0;
    int texturevar = st->usetextures && st->texturevar >= 0
			&& st->texturevar < MAXVAL
		   ? st->texturevar : -1;
    int usearea = st->polyarea;
    int sizevar = st->polysizevar;
    float polysize = st->polysize;
    float mins2d;

    int txno;

    static int txdebug = -1;

    if(txdebug == -1) {
	if(getenv("TXDEBUG"))
	    txdebug = atoi(getenv("TXDEBUG"));
	else
	    txdebug = 0;
    }

   if(!(txdebug&1)) {
    glMatrixMode( GL_TEXTURE );
    glLoadIdentity();
    glTranslatef( .5, .5, 0 );
    glScalef( st->txscale, st->txscale, st->txscale );
   }
    glMatrixMode( GL_MODELVIEW );

    /* Textures seem to be multiplied by some mysterious factor,
     * unless we're in GL_SMOOTH mode.  What gives?
     */
   if(!(txdebug&2))
    glShadeModel( texturevar >= 0 ? GL_SMOOTH : GL_FLAT );


    polyminrad = st->polymin * radperpix;
    polymaxrad = st->polymax * radperpix;
    mins2d = polyminrad*polyminrad;

    if(sizevar == -1) {
	/* If polygon size is tied to point size,
	 * then include pointsize scale factors in polygon scaling.
	 */
      if((unsigned int)st->sizedby <= MAXVAL
		&& (unsigned int)st->curdata < MAXFILES
		&& st->vdesc[st->curdata][st->sizedby].lum != 0)
	polysize *= st->vdesc[st->curdata][st->sizedby].lum;
      if(st->subsample > 0 && st->everycomp)
        polysize *= st->subsample; /* Compensate for "every" subsampling */
    }

    if(st->depthsort && !inpick) {
	depth_fwd = fwd;
	depth_d = fwdd;
	sortedpolys( st, slhead, &Tc2w, radperpix, polysize );

    } else {
      int usepolymax = (st->polymax < 1e8);

#ifdef POLYFADE
      int polyfade = 0;
      if(st->polyfademax > st->polymin) {
	polyfade = 1;
	polyfadesize = 
	glEnable(GL_BLEND);
	glBlendFunc( GL_SRC_ALPHA, additive_blend ? GL_ONE : GL_ONE_MINUS_SRC_ALPHA );
      }
#endif

      for(sl = slhead, slno = 1; sl != NULL; sl = sl->next, slno++) {
	if(sl->text != NULL || sl->special != SPECKS) continue;
	if(inpick) {
	    glLoadName(slno);
	    glPushName(0);
	}
	for(i = 0, p = sl->specks; i < sl->nspecks; i+=skip, p = NextSpeck( p, sl, skip )) {
	    float dist = VDOT( &p->p, &fwd ) + fwdd;
	    float size;

	    if(!SELECTED(sl->sel[i], &seesel))
		continue;

	    size = p->val[sizevar] * polysize;

	    if(dist + size <= 0) continue;
	    if(usearea) {
		if(size < dist * dist * mins2d)
		    continue;
		size = sqrtf(size);
	    } else {
		if(size < dist * polyminrad)
		    continue;
	    } 
	    if(usepolymax && size > dist * polymaxrad && dist > 0)
		size = dist * polymaxrad;

	    rgba = p->rgba & RGBBITS;
	    if(rgba != prevrgba) {
		prevrgba = rgba;
		rgba = RGBALPHA( prevrgba, alpha );
		glColor4ubv( (GLubyte *)&rgba );
	    }
	    if(st->polyorivar0 >= 0 && p->val[st->polyorivar0] < 9) {
		for(k = 0; k < nxyfan; k++) {
		    vcomb( &fan[k],
			size*xyfan[k][0], (Point *)&p->val[st->polyorivar0],
			size*xyfan[k][1], (Point *)&p->val[st->polyorivar0+3] );
		}
	    } else if(p->size != prevsize) {
		float s = scl*size;
		for(k = 0; k < nxyfan; k++) {
		    vcomb( &fan[k], s*xyfan[k][0], (Point *)&Tc2w.m[0*4+0],
				    s*xyfan[k][1], (Point *)&Tc2w.m[1*4+0] );
		}
		prevsize = size;
	    }

#define PFAN(vno, comp)  p->p.x[comp] + fan[vno].x[comp]

	    if(inpick) {
		glLoadName( i );
		glBegin( GL_TRIANGLE_FAN );
		for(k = 0; k < nxyfan; k++) {
		    glVertex3f( PFAN(k,0), PFAN(k,1), PFAN(k,2) );
		}
		glEnd();

	    } else if(texturevar >= 0
		    && (txno = p->val[texturevar]) >= 0
		    && txno < st->ntextures &&
		    st->textures[txno] != NULL) {

		txbind( st->textures[txno], &texturing );

		glBegin( GL_TRIANGLE_FAN );
		for(k = 0; k < nxyfan; k++) {
		    glTexCoord2fv( &xyfan[k][0] );
		    glVertex3f( PFAN(k,0), PFAN(k,1), PFAN(k,2) );
		}
		glEnd();

	    } else {
		if(texturing) {
		    texturing = 0;
		    glDisable( GL_TEXTURE_2D );
		}
		glBegin(GL_TRIANGLE_FAN);
		for(k = 0; k < nxyfan; k++)
		    glVertex3f( PFAN(k,0), PFAN(k,1), PFAN(k,2) );
		glEnd();
	    }
#undef PFAN

	}
	if(inpick) glPopName();
      }
    }
    if(texturing) {
	txbind( NULL, NULL );
	glDisable( GL_TEXTURE_2D );
    }
    glMatrixMode( GL_TEXTURE );
    glLoadIdentity();
    glMatrixMode( GL_MODELVIEW );
  }

  if(st->usepoint && !(st->useboxes == 2)) {

#define MAXPTSIZE 32	/* in half-point units */
#define PERBUCKET 64	/* max points per bucket */

    struct cpoint sized[MAXPTSIZE*2][PERBUCKET];
    int nsized[MAXPTSIZE*2];
    unsigned char invgamma[256];
    float invgam = (st->gamma <= 0) ? 0 : 1/st->gamma;

    for(i = 0; i < 256; i++)
	invgamma[i] = (int) (255.99 * pow( i/255., invgam ));

    if(inpick) {
	for(sl = slhead, slno = 1; sl != NULL; sl = sl->next, slno++) {
	    if(sl->text != NULL || sl->special != SPECKS) continue;
	    glLoadName(slno);
	    glPushName(0);
	    for(i = 0, p = sl->specks; i < sl->nspecks; i+=skip, p = NextSpeck(p, sl, skip)) {
		if(!SELECTED(sl->sel[i], &seesel))
		    continue;

		glLoadName(i);
		glBegin(GL_POINTS);
		glVertex3fv( &p->p.x[0] );
		glEnd();
	    }
	    glPopName();
	}

    } else if(fast) {
	static unsigned char apxsize[MAXPTSIZE*MAXPTSIZE];
	unsigned char faintrand[256];
	int pxsize, oldpxsize;
	int pxmin, pxmax;

	pxmin = 256 * st->pfaint;
	if(st->plarge > MAXPTSIZE) st->plarge = MAXPTSIZE;
	pxmax = 256 * st->plarge * st->plarge;

	if(apxsize[1] == 0) {
	    for(i=0; i<COUNT(apxsize); i++)
		apxsize[i] = (int)ceil(sqrtf(i+1));
	}
	for(i = 0; i < 256; i++)
	    faintrand[i] = randskip[i] * st->pfaint;

	/* Render using fast (non-antialiased) points */
	glDisable( GL_POINT_SMOOTH );
	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, additive_blend ? GL_ONE : GL_ONE_MINUS_SRC_ALPHA );

	prevrgba = 0;
	prevsize = 0;

	sl = slhead;

	pxsize = oldpxsize = 1;
	glPointSize( pxsize );
	glColor4ubv( (GLubyte *)&rgba );

	for(i = 0; i < MAXPTSIZE*2; i++)
	    nsized[i] = 0;

	glBegin( GL_POINTS );
	for(sl = slhead; sl != NULL; sl = sl->next) {
	    if(sl->text != NULL || sl->special != SPECKS) continue;
	    for(i = 0, p = sl->specks; i < sl->nspecks; i+=skip, p= NextSpeck(p, sl, skip)) {
		int lum, myalpha;
		float dist = VDOT( &p->p, &fwd ) + fwdd;
		if(dist <= 0)	/* Behind eye plane */
		    continue;

		if(!SELECTED(sl->sel[i], &seesel))
		    continue;

		if(useclip &&
		  (p->p.x[0] < clipp0.x[0] ||
		   p->p.x[0] > clipp1.x[0] ||
		   p->p.x[1] < clipp0.x[1] ||
		   p->p.x[1] > clipp1.x[1] ||
		   p->p.x[2] < clipp0.x[2] ||
		   p->p.x[2] > clipp1.x[2]))
		    continue;


		lum = 256 * plum * p->size / (dist*dist);

		if(lum < pxmin) {
		    if(lum <= faintrand[(i*i+i) /*randix++*/ & 0xFF])
			continue;
		    pxsize = 1;
		    myalpha = pxmin;
		} else if(lum < 256) {
		    pxsize = 1;
		    myalpha = lum;
		} else if(lum < pxmax) {
		    pxsize = apxsize[lum>>8];
		    myalpha = lum / (pxsize*pxsize);
		} else {
		    /* Could use a polygon here, instead. */
		    pxsize = st->plarge;
		    myalpha = 255;
		}
#ifdef USE_PTRACK
		if(useptrack) printf("pfast %d %d %d %d\n", lum, pxsize, myalpha, invgamma[myalpha] & 0xFC);
#endif

		if(pxsize < 1 || pxsize > 6 || myalpha <= 0 || myalpha > 255) {
		    static int oops;
		    oops++;
		}
		if(myalpha < 0 || myalpha > 255 || invgamma[myalpha] <= 0 || invgamma[myalpha] > 255) {
		    static int oops2;
		    oops2++;
		}
		rgba = RGBALPHA( p->rgba&RGBBITS, invgamma[myalpha] & 0xFC );

		if(pxsize != oldpxsize) {
		    if(nsized[pxsize] >= PERBUCKET) {
			glEnd();
			glPointSize(pxsize);
			glBegin( GL_POINTS );
			dumpcpoints( &sized[pxsize][0], PERBUCKET );
			nsized[pxsize] = 0;
			oldpxsize = pxsize;
			glColor4ubv( (GLubyte *)&rgba );
			prevrgba = rgba;
			glVertex3fv( &p->p.x[0] );

		    } else {
			struct cpoint *cp = &sized[pxsize][nsized[pxsize]];
			cp->rgba = rgba;
			cp->p = p->p;
			nsized[pxsize]++;
		    }
		} else {
		    /* same as current pointsize */
		    if(rgba != prevrgba) {
			glColor4ubv( (GLubyte *)&rgba );
			prevrgba = rgba;
		    }

		    glVertex3fv( &p->p.x[0] );
		}
	    }
	}
	glEnd();

	for(i = 0; i < MAXPTSIZE; i++) {
	    if(nsized[i] > 0) {
		glPointSize( i );
		glBegin( GL_POINTS );
		dumpcpoints( &sized[i][0], nsized[i] );
		glEnd();
	    }
	}

    } else {

	/* 
	 * Render using anti-aliased points
	 */
	static unsigned char apxsize[(MAXPTSIZE*2)*(MAXPTSIZE*2)];
	unsigned char faintrand[256];
	static float percoverage[MAXPTSIZE*2];
	static int needMesaHack = 0;
	int pxsize, oldpxsize;
	int pxmin, pxmax;
	int minsize, minalpha;

	pxmin = (256 * (2*2)) * st->pfaint;
	if(st->plarge > MAXPTSIZE*2) st->plarge = MAXPTSIZE*2;
	pxmax = (256 * (2*2)) * st->plarge * st->plarge;

	if(apxsize[1] == 0) {
	    char *version = (char *)glGetString( GL_VERSION );

	    /* Hack: MESA doesn't handle small anti-aliased points well;
	     * force them all to be of size 1.0 or 2.0 (i.e. pxsize == 2 or 4),
	     * and non-antialiased.
	     */
	    if(getenv("MESAHACK") != NULL)
		needMesaHack = atoi(getenv("MESAHACK"));
	    else if(version &&
		  (!strncmp(version, "CRIME", 5) || !strncmp(version, "IR", 2)))
		needMesaHack = 0;	/* O2 & IR: nice anti-aliased points */
	    else
		needMesaHack = -1;	/* IMPACT (& others?): ugly AA points */

	    for(i=0; i<COUNT(apxsize); i++) {
		apxsize[i] = (int)ceil(sqrtf(i+1));
		if(needMesaHack>0) {
		    if(apxsize[i] <= 2) apxsize[i] = 2;
		    else if(needMesaHack<2 && apxsize[i] <= 4) apxsize[i] = 4;
		}
	    }

	    for(i = 1; i < MAXPTSIZE*2; i++) {
		percoverage[i] = 1.0 / (i*i);
		if(needMesaHack>0) {
		    if(i <= 2) percoverage[i] = .25;
		    else if(needMesaHack<2 && i <= 4) percoverage[i] = .0625;
		}
	    }
	    percoverage[0] = 1.0;

	    if(needMesaHack < 0)
		percoverage[1] = 0.25;	/* if gfx treats psize 0.5 == 1.0 */

	}

	minsize = apxsize[ pxmin >> 8 ];
	minalpha = pxmin * percoverage[ minsize ];

	for(i = 0; i < 256; i++)
	    faintrand[i] = randskip[i] * st->pfaint;

	/* Render using antialiased points */
	glEnable( GL_POINT_SMOOTH );
	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, additive_blend ? GL_ONE : GL_ONE_MINUS_SRC_ALPHA );

	prevrgba = 0;
	prevsize = 0;

	sl = slhead;

	pxsize = 2;
	oldpxsize = 99;
	glPointSize( pxsize );
	glColor4ubv( (GLubyte *)&rgba );

	for(i = 0; i < MAXPTSIZE*2; i++)
	    nsized[i] = 0;

	glBegin( GL_POINTS );
	for(sl = slhead; sl != NULL; sl = sl->next) {
	    if(sl->text != NULL || sl->special != SPECKS) continue;
	    for(i = 0, p = sl->specks; i < sl->nspecks; i+=skip, p=NextSpeck(p,sl,skip)) {
		int lum, myalpha;
		float dist, dist2, dx, dy, dz;

		if(!SELECTED(sl->sel[i], &seesel))
		    continue;
		if(useclip &&
		  (p->p.x[0] < clipp0.x[0] ||
		   p->p.x[0] > clipp1.x[0] ||
		   p->p.x[1] < clipp0.x[1] ||
		   p->p.x[1] > clipp1.x[1] ||
		   p->p.x[2] < clipp0.x[2] ||
		   p->p.x[2] > clipp1.x[2]))
		    continue;


		switch(fademodel) {
		case F_PLANAR:
		    dist = VDOT( &p->p, &fwd ) + fwdd;
		    if(dist <= 0)	/* Behind eye plane */
			continue;
		    dist2 = dist*dist;
		    break;
		case F_CONSTANT:
		    dist2 = orthodist2;
		    break;
		case F_SPHERICAL:
		    dx = p->p.x[0]-eyepoint.x[0];
		    dy = p->p.x[1]-eyepoint.x[1];
		    dz = p->p.x[2]-eyepoint.x[2];
		    dist2 = dx*dx + dy*dy + dz*dz;
		    break;

		case F_LREGION:	/* not impl yet */
		    dist = VDOT( &p->p, &fwd ) + fwdd;
		    if(dist <= 0)	/* Behind eye plane */
			continue;
		    dx = p->p.x[0] - fadecen.x[0];
		    dy = p->p.x[1] - fadecen.x[1];
		    dz = p->p.x[2] - fadecen.x[2];
		    dist2 = dist * st->fadeknee2
				/ (1 + (dx*dx + dy*dy + dz*dz) * faderball2);
		    break;
		case F_LINEAR:
		    dist = VDOT( &p->p, &fwd ) + fwdd;
		    if(dist <= 0)	/* Behind eye plane */
			continue;
		    dist2 = dist * st->fadeknee2;
		    break;
			
		case F_KNEE2:
		    dx = p->p.x[0]-eyepoint.x[0];
		    dy = p->p.x[1]-eyepoint.x[1];
		    dz = p->p.x[2]-eyepoint.x[2];
		    dist2 = dx*dx + dy*dy + dz*dz;
		    if(dist2 > knee2dist2)
			dist2 *= 1 + steep2knee2 * (dist2 - knee2dist2);
		    break;
		case F_KNEE12:
		    dx = p->p.x[0]-eyepoint.x[0];
		    dy = p->p.x[1]-eyepoint.x[1];
		    dz = p->p.x[2]-eyepoint.x[2];
		    dist2 = dx*dx + dy*dy + dz*dz;
		    if(dist2 < knee1dist2)
			dist2 = knee1dist2;
		    else if(dist2 > knee2dist2)
			dist2 *= 1 + steep2knee2 * (dist2 - knee2dist2);
		    break;
		}

		lum = (256 * (2*2)) * plum * p->size / dist2;

		if(lum <= pxmin) {
		    if(lum <= faintrand[(i*i+i) /*randix++*/ & 0xFF])
			continue;
		    pxsize = minsize;
		    myalpha = minalpha;
		} else if(lum < pxmax) {
		    pxsize = apxsize[lum>>8];
		    myalpha = lum * percoverage[pxsize];
		} else {
		    /* Could use a polygon here, instead. */
		    pxsize = 2 * st->plarge;
		    myalpha = 255;
		}
#ifdef USE_PTRACK
		if(useptrack) printf("paa %d %d/2 %d %d\n", lum, pxsize, myalpha, invgamma[myalpha] & 0xFC);
#endif

		if(pxsize < 1 || pxsize >= MAXPTSIZE*2 || myalpha <= 0 || myalpha > 255) {
		    static int oops;
		    oops++;
		    pxsize = MAXPTSIZE*2 - 1;
		    myalpha = 255;
		}
		rgba = RGBALPHA( p->rgba&RGBBITS, invgamma[myalpha] & 0xFC );

		if(pxsize != oldpxsize) {
		    if(nsized[pxsize] >= PERBUCKET) {
			glEnd();
			if(needMesaHack>0 && (pxsize <= 4) != (oldpxsize <= 4)) {
			    if(pxsize <= 4) glDisable( GL_POINT_SMOOTH );
			    else glEnable( GL_POINT_SMOOTH );
			}
			glPointSize(.5*pxsize);
			glBegin( GL_POINTS );
			dumpcpoints( &sized[pxsize][0], PERBUCKET );
			nsized[pxsize] = 0;
			oldpxsize = pxsize;
			glColor4ubv( (GLubyte *)&rgba );
			prevrgba = rgba;
			glVertex3fv( &p->p.x[0] );

		    } else {
			struct cpoint *cp = &sized[pxsize][nsized[pxsize]];
			cp->rgba = rgba;
			cp->p = p->p;
			nsized[pxsize]++;
		    }
		} else {
		    /* same as current pointsize */
		    if(rgba != prevrgba) {
			glColor4ubv( (GLubyte *)&rgba );
			prevrgba = rgba;
		    }

		    glVertex3fv( &p->p.x[0] );
		}
	    }
	}
	glEnd();

	if(needMesaHack>0)
	    glDisable( GL_POINT_SMOOTH );

	for(i = 0; i < MAXPTSIZE*2; i++) {
	    if(needMesaHack>0 && i == 4)
		glEnable( GL_POINT_SMOOTH );
	    if(nsized[i] > 0) {
		glPointSize( .5*i );
		glBegin( GL_POINTS );
		dumpcpoints( &sized[i][0], nsized[i] );
		glEnd();
	    }
	}

    }
  }

  if(st->usetext && st->textsize != 0) {
    int cment = -1;
    int textmin = abs(st->textmin);
    int label_stubs = (st->textmin < 0);
    glColor3f(1,1,1);

    for(sl = slhead, slno = 1; sl != NULL; sl = sl->next, slno++) {
	if(sl->text != NULL && (p = sl->specks) != NULL) {
	    float dist = VDOT( &p->p, &fwd ) + fwdd;
	    float tsize;

	    if(dist <= 0)
		continue;

	    if(inpick) glLoadName(slno);

	    tsize = st->textsize * p->size;
	    if(tsize < dist * (radperpix * textmin)) {
		if(label_stubs) {
		    /* Draw a stub: a simple line segment of
		     * about the right length
		     */
		    Point ep;
		    /* extract top row of Ttext -- this is the unit
		     * screen-space X vector, expressed in world coords.
		     * Scale to suit.
		     */
		    vcomb( &ep,
			p->size * sfStrWidth(sl->text), (Point *)&Ttext.m[0],
			1, &p->p );

		    if(p->rgba != cment) {
			if((unsigned int)p->rgba >= st->textncmap)
			    glColor3f( 1, 1, 1 );
			else
			    glColor3ubv( (GLubyte *)&st->textcmap[ p->rgba ].cooked );
			cment = p->rgba;
		    }
		    glBegin( GL_LINES );
		    glVertex3fv( p->p.x );
		    glVertex3fv( ep.x );
		    glEnd();
		}
		continue;
	    }

	    /* Otherwise, label is big enough to see -- draw actual text */

	    if(st->usetextaxes) {
		static unsigned char col[3][3] = {255,0,0, 0,255,0, 0,0,255};
		glBegin(GL_LINES);
		for(k = 0; k < 3; k++) {
		    glColor3ubv( &col[k][0] );
		    glVertex3fv( p->p.x );
		    glVertex3f( p->p.x[0] + (k==0?tsize:0),
				p->p.x[1] + (k==1?tsize:0), 
				p->p.x[2] + (k==2?tsize:0) );
		}
		glEnd();
	    }
	    if(p->rgba != cment || st->usetextaxes) {
		if((unsigned int)p->rgba >= st->textncmap)
		    glColor3f( 1, 1, 1 );
		else
		    glColor3ubv( (GLubyte *)&st->textcmap[ p->rgba ].cooked );
		cment = p->rgba;
	    }
	    sfStrDrawTJ( sl->text, p->size, &p->p, &Ttext, NULL );
	}
    }
  }

  if(inpick) glLoadName(0);

  if(st->usemeshes) {
    struct mesh *m;
    int texturing = 0;

    if(additive_blend && st->alpha < 1) {
	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, GL_ONE );
    } else {
	glDisable( GL_BLEND );
    }
    glColor4f(1, 1, 1, st->alpha);
    for(m = st->staticmeshes; m != NULL; m = m->next)
	specks_draw_mesh(st, m, &texturing);
    for(m = CURDATATIME(meshes); m != NULL; m = m->next)
	specks_draw_mesh(st, m, &texturing);


    if(texturing) txbind(NULL, NULL);
  }

  if(st->useellipsoids) {
    struct ellipsoid *e;

    if(additive_blend) {
	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, GL_ONE );
    } else {
	glDisable( GL_BLEND );
    }
    glColor4f(1, 1, 1, st->alpha);
    for(e = st->staticellipsoids; e != NULL; e = e->next)
	specks_draw_ellipsoid(st, e);
  }



  for(i = 0; i < 6; i++)	/* in case clipbox was enabled when we began */
    glDisable( GL_CLIP_PLANE0 + i );


  glColor4ub(255,255,255,255);
  glDisable( GL_BLEND );
  glEnable( GL_DEPTH_TEST );	/* was already enabled */
  glDepthMask( GL_TRUE );
}

void specks_draw_ellipsoid( struct stuff *st, struct ellipsoid *e )
{
  int nu = e->nu;
  int nv = e->nv;
  int u, v;
  float *su, *cu, *sv, *cv;

  if(st->boxlevelmask == 0 || e->level >= 0 && ((1 << e->level) & st->boxlevelmask) == 0)
    return;

  if(nu == 0 && nv == 0) {
    nu = 15;
    nv = 10;
  }
  su = NewA( float, nu );
  cu = NewA( float, nu );
  sv = NewA( float, nv );
  cv = NewA( float, nv );
  for(u = 0; u < nu; u++) {
    float tu = 2*M_PI*u/nu;
    su[u] = sin(tu);
    cu[u] = cos(tu);
  }
  for(v = 0; v < nv; v++) {
    float tv = M_PI*v/(nv-1);
    sv[v] = sin(tv);
    cv[v] = cos(tv);
  }
  glPushMatrix();
  glTranslatef( e->pos.x[0],e->pos.x[1],e->pos.x[2] );
  if(e->hasori)
    glMultMatrixf( &e->ori.m[0] );
  glScalef( e->size.x[0], e->size.x[1], e->size.x[2] );

  if(e->cindex >= 0) {
    int rgba =
	RGBALPHA(
	   RGBWHITE & st->cmap[(e->cindex < st->ncmap) ? e->cindex : st->ncmap-1].cooked,
	   (int)(st->alpha * 255));
    glColor4ubv((GLubyte *)&rgba);
  }

  switch(e->style) {
  case S_SOLID:
    for(v = 1; v < nv; v++) {
	glBegin( GL_QUAD_STRIP );
	for(u = 0; u < nu; u++) {
	    glVertex3f( cu[u]*sv[v-1], su[u]*sv[v-1], cv[v-1] );
	    glVertex3f( cu[u]*sv[v], su[u]*sv[v], cv[v] );
	}
	glVertex3f( cu[0]*sv[v-1], su[0]*sv[v-1], cv[v-1] );
	glVertex3f( cu[0]*sv[v], su[0]*sv[v], cv[v] );
	glEnd();
    }
    break;

  case S_LINE:
    for(v = 1; v < nv-1; v++) {
	glBegin( GL_LINE_LOOP );
	for(u = 0; u < nu; u++)
	    glVertex3f( cu[u]*sv[v], su[u]*sv[v], cv[v] );
	glEnd();
    }
    for(u = 0; u < nu; u++) {
	glBegin( GL_LINE_STRIP );
	for(v = 0; v < nv; v++)
	    glVertex3f( cu[u]*sv[v], su[u]*sv[v], cv[v] );
	glEnd();
    }
    break;
    
  case S_PLANE:
    glBegin( GL_LINE_LOOP );
    for(u = 0; u < nu; u++)
	glVertex3f( cu[u], su[u], 0 );
    glEnd();
    glBegin( GL_LINE_LOOP );
    for(u = 0; u < nu; u++)
	glVertex3f( cu[u], 0, su[u] );
    glEnd();
    glBegin( GL_LINE_LOOP );
    for(u = 0; u < nu; u++)
	glVertex3f( 0, cu[u], su[u] );
    glEnd();
    break;

  case S_POINT:
    glPointSize( 1.0 );
    glBegin( GL_POINTS );
    glVertex3f( 0,0,1 );
    glVertex3f( 0,0,-1 );
    for(v = 1; v < nv-1; v++) {
	for(u = 0; u < nu; u++)
	    glVertex3f( cu[u]*sv[v], su[u]*sv[v], cv[v] );
    }
    glEnd();
    break;
  }
  glPopMatrix();
}

void specks_draw_mesh( struct stuff *st, register struct mesh *m, int *texturing )
{
  int u, v, prev, cur;
  int usetx;
  float gap, ungap;
  int f, was;
  int usemullions = (st->mullions > 0);

  usetx = (st->usetextures && m->tx != NULL
		&& m->txno >= 0 && m->txno < st->ntextures
		&& st->textures[m->txno] != NULL);
  txbind( usetx ? st->textures[m->txno] : NULL, texturing );
  if(m->fnorms != NULL) {
    static float amb[3] = { 0,0,0 };
    static float spec[3] = { .4,.4,.4 };
    glEnable( GL_LIGHTING );
    glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE );
    glEnable( GL_COLOR_MATERIAL );
    glShadeModel( GL_SMOOTH );
    glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, 20. );
    glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, amb );
    glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, spec );

  }
  if(m->cindex >= 0) {
    int rgba =
	RGBALPHA(
	   RGBWHITE & st->cmap[(m->cindex < st->ncmap) ? m->cindex : st->ncmap-1].cooked,
	   (int)(st->alpha * 255));
    glColor4ubv((GLubyte *)&rgba);
  } else if(st->coloredby == CONSTVAL) {
    struct valdesc *vd = &st->vdesc[st->curdata][CONSTVAL];
    glColor4f( vd->cmin, vd->cmax, vd->mean, st->alpha );
  }

  switch(m->type) {
  case MODEL:
    if(m->objrender)
	(*m->objrender)( st, m );
    break;
  case QUADMESH:
    switch(m->style) {
	case S_SOLID:
	for(v = 1; v < m->nv; v++) {
	    prev = (v-1) * m->nu;
	    cur = v * m->nu;
	    glBegin( GL_QUAD_STRIP );
	    for(u = 0; u < m->nu; u++) {
		if(usetx) {
		    glTexCoord2fv( &m->tx[prev+u].x[0] );
		    glVertex3fv( &m->pts[prev+u].x[0] );
		    glTexCoord2fv( &m->tx[cur+u].x[0] );
		    glVertex3fv( &m->pts[cur+u].x[0] );
		} else {
		    glVertex3fv( &m->pts[prev+u].x[0] );
		    glVertex3fv( &m->pts[cur+u].x[0] );
		}
	    }
	    glEnd();
	}
	break;

	case S_LINE:
	    for(v = 0; v < m->nv; v++) {
		glBegin( GL_LINE_STRIP );
		cur = v * m->nu;
		for(u = 0; u < m->nu; u++)
		    glVertex3fv( &m->pts[cur+u].x[0] );
		glEnd();
	    }
	    for(u = 0; u < m->nu; u++) {
		glBegin( GL_LINE_STRIP );
		for(v = 0; v < m->nv; v++)
		    glVertex3fv( &m->pts[v*m->nu + u].x[0] );
		glEnd();
	    }
	    break;

	case S_POINT:
	    cur = m->nu * m->nv;
	    glBegin( GL_POINTS );
	    for(u = 0; u < cur; u++)
		glVertex3fv( &m->pts[u].x[0] );
	    glEnd();
	    break;

	case S_PLANE:
	    glBegin( GL_LINE_LOOP );
	    for(u = 0; u < m->nu; u++)
		glVertex3fv( &m->pts[u].x[0] );
	    for(v = 1; v < m->nv; v++)
		glVertex3fv( &m->pts[v*m->nu + m->nu-1].x[0] );
	    cur = (m->nv-1) * m->nu;
	    for(u = m->nu; --u >= 0; )
		glVertex3fv( &m->pts[cur+u].x[0] );
	    for(v = m->nv-1; --v > 0; )
		glVertex3fv( &m->pts[v*m->nu].x[0] );
	    glEnd();
	    break;
    }
    break;

  case POLYMESH:
    gap = st->mullions;
    ungap = 1 - gap;
   
    was = 0;
    for(f = 0; f < m->nfaces; f++) {
	int fv0 = m->fv0[f];
	int fvn = m->fvn[f];
	int *fvs0 = &m->fvs[ fv0 ];
	Point *pts = m->pts;
	Point bary, *pa, *pb, *pc;

	if(usemullions && fvn == 3) {
	    pa = &pts[ fvs0[0] ];
	    pb = &pts[ fvs0[1] ];
	    pc = &pts[ fvs0[2] ];
	    bary.x[0] = (pa->x[0] + pb->x[0] + pc->x[0]) * 0.3333333333;
	    bary.x[1] = (pa->x[1] + pb->x[1] + pc->x[1]) * 0.3333333333;
	    bary.x[2] = (pa->x[2] + pb->x[2] + pc->x[2]) * 0.3333333333;
#define INTERP(p) p->x[0]*gap + bary.x[0]*ungap, p->x[1]*gap + bary.x[1]*ungap, p->x[2]*gap + bary.x[2]*ungap
	    if(was) {
		was = 0;
		glEnd();
	    }
	    glBegin( GL_QUAD_STRIP );
	    glNormal3fv( m->fnorms[f].x );
	    glVertex3fv(pa->x); glVertex3f( INTERP(pa) );
	    glVertex3fv(pb->x); glVertex3f( INTERP(pb) );
	    glVertex3fv(pc->x); glVertex3f( INTERP(pc) );
	    glVertex3fv(pa->x); glVertex3f( INTERP(pa) );
	    glEnd();
	} else if(fvn == 3) {
	    if(was != GL_TRIANGLES) {
		if(was) glEnd();
		glBegin( was=GL_TRIANGLES );
	    }
	    glNormal3fv( m->fnorms[f].x );
	    glVertex3fv( pts[ fvs0[0] ].x );
	    glVertex3fv( pts[ fvs0[1] ].x );
	    glVertex3fv( pts[ fvs0[2] ].x );
	} else if(fvn == 4) {
	    if(was != GL_QUADS) {
		if(was) glEnd();
		glBegin( was=GL_QUADS );
	    }
	    glNormal3fv( m->fnorms[f].x );
	    glVertex3fv( pts[ fvs0[0] ].x );
	    glVertex3fv( pts[ fvs0[1] ].x );
	    glVertex3fv( pts[ fvs0[2] ].x );
	    glVertex3fv( pts[ fvs0[3] ].x );
	} else if(fvn < 0) {
	    if(was) {
		glEnd();
		was = 0;
	    }
	    glBegin( GL_TRIANGLE_STRIP );
	    glNormal3fv( m->fnorms[f].x );
	    do {
		glVertex3fv( pts[ *fvs0++ ].x );
	    } while(++fvn < 0);
	    glEnd();
	} else {
	    if(was) {
		glEnd();
		was = 0;
	    }
	    glBegin( GL_POLYGON );
	    glNormal3fv( m->fnorms[f].x );
	    do {
		glVertex3fv( pts[ *fvs0++ ].x );
	    } while(--fvn > 0);
	    glEnd();
	}
    }
    if(was) glEnd();
    break;
  }
  if(m->fnorms != NULL) {
    glDisable( GL_LIGHTING );
    glDisable( GL_COLOR_MATERIAL );
  }
}

void specks_draw_boxes( struct stuff *st, struct AMRbox *boxes, int boxlevelmask, Matrix Ttext, int oriented )
{
  static short arcs[] = {
	5, 4, 6, 2, 3, 1, 5, 7, 6,
	-1,
	7, 3,
	0, 1,
	0, 2,
	0, 4
  };
  struct AMRbox *box;
  int i;
  float boxscale, s0, s1;
  int isscaled;

  glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
  glLineWidth( st->boxlinewidth );
  glEnable( GL_BLEND );
  glDisable( GL_LINE_SMOOTH );

  /* Scan through the array, whose end is marked with a box at level -1. */
  for(box = boxes; box->level >= 0; box++) {
    boxscale = (box->level < 0) ? 1.0
	   : st->boxscale[ box->level>=MAXBOXLEV ? MAXBOXLEV-1 : box->level ];
    isscaled = (boxscale != 0 && boxscale != 1);
    if(isscaled) {
	s0 = .5*(1 + boxscale);
	s1 = .5*(1 - boxscale);
    }
    if((1 << box->level) & boxlevelmask) {
	int cval = RGBALPHA(st->boxcmap[
		    box->level<0 ? 0
		    : box->level>=st->boxncmap ?
			st->boxncmap-1 : box->level ].cooked,
		    0xFF);
	int colorme = 0;

	if(oriented)
	    cval = orientboxcolor;
	glColor4ubv( (GLubyte *)&cval );
	glBegin( GL_LINE_STRIP );
	for(i = 0; i < COUNT(arcs); i++) {
	    int vert = arcs[i];
	    if(vert < 0) {
		glEnd();
		glBegin( GL_LINES );
	    } else {

		if(oriented) {
		    if(vert == 0) {
			glColor4ubv( (GLubyte *)&cval );
			colorme = 1;
		    } else if(colorme) {
			glColor3f( vert&1?1:0, vert&2?1:0, vert&4?1:0 );
		    }
		}

		if(isscaled && !oriented && box->level >= 0) {
		    glVertex3f(
			vert&1	? s0*box->p0.x[0] + s1*box->p1.x[0]
				: s1*box->p0.x[0] + s0*box->p1.x[0],
			vert&2	? s0*box->p0.x[1] + s1*box->p1.x[1]
				: s1*box->p0.x[1] + s0*box->p1.x[1],
			vert&4	? s0*box->p0.x[2] + s1*box->p1.x[2]
				: s1*box->p0.x[2] + s0*box->p1.x[2] );
		} else {
		    glVertex3f(
			(vert&1 ? box->p1.x : box->p0.x)[0],
			(vert&2 ? box->p1.x : box->p0.x)[1],
			(vert&4 ? box->p1.x : box->p0.x)[2]
		    );
		}
	    }
	}
	glEnd();
	if(st->boxlabels && st->boxlabelscale != 0) {
	    float sz = .16 * vdist( &box->p0, &box->p1 ) / st->textsize;
	    Point mid;
	    char lbl[16];
	    vlerp( &mid, .5, &box->p0, &box->p1 );
	    sprintf(lbl, "%d", box->boxno);
	    sfStrDrawTJ( lbl, sz, &mid, &Ttext, "c" );
	}
    }
  }
  glLineWidth( 1 );
}

int specks_partial_pick_decode( struct stuff *st, int id,
			int nhits, int nents, GLuint *hitbuf,
			unsigned int *bestzp, struct specklist **slp,
			int *specknop, Point *pos )
{
  int i, hi, ns, slno;
  GLuint z0, bestz = *bestzp;
  int bestslno = 0, bestspeck = -1;
  struct specklist *sl, *slhead;

  if(st == NULL)
    return 0;

  if(st->picked != NULL)
    (*st->picked)(st, NULL, NULL, nhits);	/* "prepare for up to nhits calls" */

  slhead = st->frame_sl;		/* st->sl as snapped by specks_ffn() */
  if(slhead == NULL) slhead = st->sl;	/* maybe there is no specks_ffn() */
  if(slhead == NULL) return 0;

  for(hi = 0, i = 0; hi < nhits && i < nents; hi++, i += ns + 3) {
    ns = hitbuf[i];
    if(ns < 1 || ns > 16)
	break;			/* trouble */
    z0 = hitbuf[i+1];

    if(id != hitbuf[i+3] || ns <= 1) continue;

    if(st->usepoly>1)		/* debug */
	printf(ns>1?"[%x %d/%d]":"[%x %d]", z0, hitbuf[i+3],hitbuf[i+4]);

    if(bestz > z0 && ns > 1 && (slno = hitbuf[i+4]) > 0) {
	bestz = z0;
	bestslno = slno;
	bestspeck = (ns>2) ? hitbuf[i+5] : 0;
    }
    if(st->picked != NULL && ns > 2) {
	for(sl = slhead, slno = 1; sl != NULL && slno < hitbuf[i+4]; sl = sl->next, slno++)
	    ;
	if(sl && hitbuf[i+5] < sl->nspecks)
	    if((*st->picked)(st, &hitbuf[i], sl, hitbuf[i+5]))
		return 0;
    }
  }

  if(st->picked != NULL) {
    if((*st->picked)(st, NULL, NULL, 0))
	return 0;
  }
  if(bestslno <= 0)
    return 0;

  for(sl = slhead, slno = 1; sl != NULL; sl = sl->next, slno++) {
    if(slno == bestslno) {
	if(bestspeck < 0 || bestspeck >= sl->nspecks) {
	    msg("Bogus pick result: sl %x bestspeck %d of 0..%d!\n",
		sl, bestspeck, sl->nspecks-1);
	    return 0;
	}
	if(slp) *slp = sl;
	if(specknop) *specknop = bestspeck;
	if(pos) {
	    if(bestspeck < 0 || bestspeck >= sl->nspecks)
		bestspeck = 0;
	    *pos = NextSpeck( sl->specks, sl, bestspeck )->p;
	}
	*bestzp = bestz;
	return 1;
    }
  }
  return 0;
}

int selname( struct stuff *st, char *name, int create ) {
    int i, avail;
    if(name == NULL) return 0;
    if(!strcmp(name, "all") || !strcmp(name, "on")) return SEL_ALL;
    if(!strcmp(name, "none") || !strcmp(name, "off")) return SEL_OFF;
    if(!strcmp(name, "thresh")) return SEL_THRESH;
    if(!strcmp(name, "pick")) return SEL_PICK;
    for(i = COUNT(st->selitems); --i >= 0; ) {
	if(!strcmp(name, st->selitems[i].name))
	    return i+1;
	if(st->selitems[i].name[0] == '\0') avail = i;
    }
    if(create) {
	sprintf(st->selitems[avail].name, "%.15s", name);
	return avail+1;
    }
    return 0;
}

void selinit( SelOp *sp ) {
    sp->wanted = 0;
    sp->wanton = 0;
    sp->use = SEL_NONE;
}

static char *selcat( struct stuff *st, char *from, int selno, int preop ) {
    char *what = "";
    static char s[8];
    switch(selno) {
    case SEL_NONE: what = "off"; break;
    case SEL_ALL: what = "all"; break;
    case SEL_THRESH: what = "thresh"; break;
    case SEL_PICK: what = "pick"; break;
    default:
	if(selno > 0 && selno <= MAXSEL && st->selitems[selno-1].name[0] != '\0')
	    what = &st->selitems[selno-1].name[0];
	else {
	    sprintf(s, "%d", selno<0||selno>MAXSEL?0:selno);
	    what = s;
	}
    }
    if(preop)
	*from++ = preop;
    return from + sprintf(from, "%.15s ", what);
}


char *show_selexpr( struct stuff *st, SelOp *destp, SelOp *srcp )
{
    char str[(MAXSELNAME + 2)*33];
    char *tail = str;
    static char *sstr;
    static int sroom = -1;
    int i;

    if(destp) {
	if(destp->wanted == 0) {
	    tail = selcat(st, tail, SEL_ALL, 0 );
	} else {
	    for(i = SEL_THRESH; i > 0; i--) {
		if(~destp->wanted & SELMASK(i)) {
		    tail = selcat( st, tail, i,
				destp->wanton & SELMASK(i) ? 0 : '-' );
		} else if(destp->wanton & SELMASK(i)) {
		    tail = selcat( st, tail, i, '^' );
		}
	    }
	}
	if(srcp)
	    tail += sprintf(tail, "= ");
    }
    if(srcp) {
	if(srcp->use == SEL_NONE) {
	    tail = selcat( st, tail, srcp->use, 0 );
	} else if(srcp->use == SEL_USE && srcp->wanted == 0) {
	    tail = selcat( st, tail, SEL_ALL, 0 );
	} else {
	    for(i = SEL_THRESH; i > 0; i--) {
		if(srcp->wanted & SELMASK(i)) {
		    tail = selcat( st, tail, i,
				srcp->wanton & SELMASK(i) ? 0 : '-' );
		} else if(srcp->wanton & SELMASK(i)) {
		    tail = selcat( st, tail, i, '^' );
		}
	    }
	}
    }
    if(tail - str > sroom) {
	if(sstr) Free(sstr);
	sroom = tail - str + 60;
	sstr = NewN( char, sroom );
    }
    if(tail == str) strcpy(sstr, "null");
    else {
	tail[-1] = '\0';
	strcpy(sstr, str);
    }
    return sstr;
}

enum SelToken {
    SEL_ERR,		/* unknown */
    SEL_EOF,
    SEL_WORD,		/* bare word or +word */
    SEL_EQUALS		/* = */
};

static enum SelToken seltoken( char **sp, int *leadcp, char word[MAXSELNAME] ) {
    char *p = *sp;
    int k;
    *leadcp = '\0';
    word[0] = '\0';
    while(isspace(*p)) p++;
    if(*p == '\0')
	return SEL_EOF;
    if(*p == '-' || *p == '^' || *p == '+')
	*leadcp = *p++;
    if(*p == '=') {
	*sp = p+1;
	return SEL_EQUALS;
    }
    if(!(isalnum(*p) || *p == '_'))
	return SEL_ERR;
    word[0] = *p++;
    for(k = 1; isalnum(*p) || *p == '_'; p++) {
	if(k < MAXSELNAME-1) word[k++] = *p;
    }
    word[k] = '\0';
    *sp = p;
    return SEL_WORD;
}

void seldest2src( struct stuff *st, CONST SelOp *dest, SelOp *src ) {
    selinit( src );
    if(dest->use == SEL_USE) {
	*src = *dest;
    } else if(dest->use == SEL_DEST) {
	src->wanted = ~dest->wanted | dest->wanton;
	src->wanton = dest->wanton;
	src->use = SEL_USE;
    }
}

void selsrc2dest( struct stuff *st, CONST SelOp *src, SelOp *dest ) {
    *dest = *src;
    if(src->use == SEL_USE) {
	dest->wanted = ~src->wanted;
	dest->wanton = src->wanton & src->wanted;
	dest->use = SEL_DEST;
    }
}

void seldestinvert( struct stuff *st, SelOp *dest ) {
    dest->wanton ^= ~dest->wanted;
}

int seldest( struct stuff *st, SelOp *dest, int selno, int selch ) {
    SelMask mask;
    if(dest == NULL)
	return 0;
    if(selno == SEL_ALL) {
	mask = ~0;
    } else if(selno <= 0 || selno > 32) {
	return 0;
    } else {
	mask = SELMASK(selno);
    }
    if(dest->use != SEL_DEST) {
	dest->use = SEL_DEST;
	dest->wanted = ~0;
	dest->wanton = 0;
    }
    switch(selch) {
    case '^':
	dest->wanted |= mask;
	dest->wanton |= mask;
	break;
    case '-':
	dest->wanted &= ~mask;
	dest->wanton &= ~mask;
	break;
    case '+':
    case '\0':
	dest->wanted &= ~mask;
	dest->wanton |= mask;
	break;
    default:
	return 0;
    }
    return 1;
}

int selsrc( struct stuff *st, SelOp *src, int selno, int selch ) {
    SelMask mask;
    if(src == NULL)
	return 0;
    if(src->use == SEL_DEST) {
	src->wanted = 0;
	src->wanton = 0;
	src->use = SEL_USE;
    }
    if(selno == SEL_OFF) {
	src->wanted = src->wanton = mask = ~0;
	src->use = SEL_NONE;
    } else if(selno == SEL_ALL) {
	src->wanted = src->wanton = mask = 0;
	src->use = SEL_USE;
    } else if(selno > 0 && selno <= 32) {
	mask = SELMASK(selno);
	src->use = SEL_USE;
    } else {
	return 0;
    }
    switch(selch) {
    case '-':
	src->wanted |= mask;
	src->wanton &= ~mask;
	break;
    case '+':
    case '\0':
	src->wanted |= mask;
	src->wanton |= mask;
	break;
    default:
	return 0;
    }
    return 1;
}

/*
 * [word [op]=] { [op]word }*
 */
int parse_selexpr( struct stuff *st, char *str, SelOp *destp, SelOp *srcp, char *plaint )
{
  SelOp dest, src;
  char wd[MAXSELNAME], w[MAXSELNAME];
  char *p = str;
  int leadc, leadc2;
  enum SelToken tok, tok2;
  int ok = 1;
  int anysrc = 0, anydest = 0;

  selinit(&dest); selinit(&src);
  tok = seltoken( &p, &leadc, wd );
  if(tok == SEL_EOF)
      return 0;
  if(destp) {
      /* Had better begin with a word, and with no prefix */
    int destno;
    if(tok != SEL_WORD) goto fail;
    destno = selname( st, wd, 1 );
    tok2 = seltoken( &p, &leadc2, w );
    if(tok2 == SEL_ERR) goto fail;
    ok = seldest( st, &dest, destno, leadc ? leadc : leadc2 );
    anydest = 1;
    if(!ok) goto fail;
    if(tok2 == SEL_WORD) {
	tok = SEL_WORD;
	leadc = leadc2;
	strcpy(wd, w);
    } else {
	tok = seltoken( &p, &leadc, wd );
    }
  }
  while(srcp && tok == SEL_WORD) {
    ok = selsrc( st, &src, selname( st, wd, 0 ), leadc );
    if(!ok) goto fail;
    if(src.use == SEL_NONE && src.wanted != 0 && destp) {
	seldestinvert( st, &dest );
	selsrc( st, &src, SEL_ALL, 0 );	/* X = off  <=> -X = on */
    }
    anysrc = 1;
    tok = seltoken( &p, &leadc, wd );
  }
  if(tok != SEL_EOF)
      goto fail;
  if(destp && anydest) *destp = dest;
  if(srcp && anysrc) *srcp = src;
  return (anydest?1:0) + (anysrc?2:0);

fail:
  if(plaint)
      msg("%s: um, %s", plaint, str);
  return 0; /*XXX*/
}

CONST char *selcounts( struct stuff *st, struct specklist *sl, SelOp *selp ) {
    int i;
    static char counts[24];
    int yes, total;
    SelOp selop;

    if(selp == NULL) return "";
    if(selp->use == SEL_DEST)
	seldest2src( st, selp, &selop );
    else
	selop = *selp;
    for(yes = total = 0; sl != NULL; sl = sl->next) {
	SelMask *sel = sl->sel;
	if(sl->text != NULL || sl->nsel < sl->nspecks || sl->special != SPECKS)
	    continue;
	total += sl->nspecks;
	for(i = 0; i < sl->nspecks; i++)
	    if(SELECTED(sel[i], &selop))
		yes++;
    }
    if(selop.use == SEL_NONE) yes = 0;
    sprintf(counts, "%d of %d", yes, total);
    return counts;
}

SpecksPickFunc specks_all_picks( struct stuff *st, SpecksPickFunc func, void *arg ) {
    SpecksPickFunc was = st->picked;
    st->picked = func;
    st->pickinfo = arg;
    return was;
}

#if CAVE

void parti_seto2w( struct stuff *st, int objno, CONST Matrix *newTo2w ) {
    st->objTo2w = *newTo2w;	/* ignores objno -- there's only one right now! */
}

void parti_geto2w( struct stuff *st, int objno, Matrix *curTo2w ) {
    *curTo2w = st->objTo2w;	/* ignores objno -- there's only one right now! */
}
    
void specks_display( struct stuff *st )
{
    int showmenus;
    int menuhit = 0;
    int empty = st->ntimes == 0 && st->sl == NULL;
    static int hidescene;

    if( CAT_dsp_mode == APP_DSP_NORMAL ) {

#if !(MENU_IN_PPR)
	if( CAVEEye == CAVE_LEFT_EYE ) {
	    menu_check( pmenu, pmenu->cavewall );
	    menu_check( stubmenu, stubmenu->cavewall );

	    menu_sethidden( pmenu, st->hidemenu || empty );
	    menu_sethidden( stubmenu, st->hidemenu<=0 || empty );
	}
#endif

	glPushMatrix();
	/* The following only works if our transformation is exactly the
	 * Cave nav transform, i.e. if the CAT system hasn't
	 * translated us further; but that's true for this app.
	 */
	CAVENavInverseTransform();

	menu_draw( pmenu );

	if(!pmenu->hidden) {
	    menuhit = (pmenu->wallhit > 0);
	    menu_setpos( stubmenu, ment.menu->x0, ment.menu->y0 );
	}
	menu_draw( stubmenu );

	glPopMatrix();

    }

    if(hidescene && !menuhit)
	hidescene = 0;
    if(menuhit && *CAVEFramesPerSecond < st->menudemandfps)
	hidescene = 1;		/* Don't draw scene if too slow to operate menu easily */
    if(hidescene)
	return;

    glPushMatrix();
    glTranslatef( st->gtrans.x[0], st->gtrans.x[1], st->gtrans.x[2] );
    glScalef( st->gscale, st->gscale, st->gscale );
    glMultMatrixf( st->objTo2w.m );
    drawspecks( st );
    glPopMatrix();
}
#endif /*NOCAVE*/

static void addchunk( struct stuff *st, int nsp, int bytesperspeck,
			float scaledby, struct speck *sp, char *text,
			int outbytesperspeck )
{
    struct specklist **slp, *sl = NewN( struct specklist, 1 );
    memset( sl, 0, sizeof(*sl) );
    sl->speckseq = ++st->speckseq;

    sl->bytesperspeck = outbytesperspeck;

    sl->specks = NewNSpeck( sl, nsp );
    sl->nspecks = nsp;
    sl->scaledby = scaledby;
    if(text) {
	sl->text = NewN( char, strlen(text)+1 );
	strcpy(sl->text, text);
    } else {
	sl->text = NULL;
    }
    if(bytesperspeck == outbytesperspeck) {
	memcpy( sl->specks, sp, nsp*sl->bytesperspeck );
    } else {
	int i;
	for(i = 0; i < nsp; i++)
	    memcpy(((char *)sl->specks) + i*outbytesperspeck,
		   ((char *)sp) + i*bytesperspeck,
		   outbytesperspeck);
    }
    sl->sel = NewN( SelMask, nsp );
    sl->nsel = nsp;
    memset(sl->sel, 0, nsp*sizeof(SelMask));

    slp = specks_timespecksptr(st, st->curdata, st->datatime);
    sl->next = *slp;
    *slp = sl;
    st->sl = specks_timespecks(st, st->curdata, st->curtime); /* in case it changed */
    sl->colorseq = -1;		/* Force recomputing colors */
    sl->sizeseq = -1;		/* Force recomputing sizes */
}

int specks_count( struct specklist *sl ) {
    int n;
    for(n = 0; sl != NULL; sl = sl->next)
	if(sl->text == NULL)
	    n += sl->nspecks * (sl->subsampled>0 ? sl->subsampled : 1);
    return n;
}


static float speckscale = 1;

#define LINEBUFSIZE 2048

static char *getline(FILE *f, char *buf) {
  buf[0] = '\0';
  while(fgets(buf, LINEBUFSIZE, f) != NULL) {
    char *cp = buf;
    while(isspace(*cp)) cp++;
    if(*cp != '#' && *cp != '\0')
	return cp;
  }
  return NULL;
}

int tokenize(char *str, char *tbuf, int maxargs, char **argv, char **commentp)
{
  register char *ip = str;
  register char *op = tbuf;
  int argc = 0;

  if(commentp) *commentp = NULL;

  if(strchr(str, '$'))
      ip = str = envexpand(str);

  while(argc < maxargs-1) {
    while(isspace(*ip)) ip++;
    if(*ip == '\0') goto eol;
    argv[argc++] = op;
    switch(*ip) {

    case '#':
	if(commentp != NULL) {
	    *commentp = op;
	    while(*ip && *ip != '\n' && *ip != '\r')
		*op++ = *ip++;
	    *op = '\0';
	}
	argc--;
	goto eol;

    case '"':
	ip++;
	while(*ip != '"') {
	    if(*ip == '\\') ip++;
	    if(*ip == '\0') goto eol;
	    *op++ = *ip++;
	}
	ip++;
	break;

    case '\'':
	ip++;
	while(*ip != '\'') {
	    if(*ip == '\\') ip++;
	    if(*ip == '\0') goto eol;
	    *op++ = *ip++;
	}
	ip++;
	break;

    default:
	while(!isspace(*ip)) {
	    if(*ip == '\\') ip++;
	    if(*ip == '\0') goto eol;
	    *op++ = *ip++;
	}
	break;
    }
    *op++ = '\0';
  }
  /* oh no, too many entries for argv[] array --
   * leave all remaining chars in last argument.
   */
  strcpy(op, ip);
  argv[argc++] = op;
 eol:
  *op = '\0';
  argv[argc] = NULL;
  return argc;
}

char *rejoinargs( int arg0, int argc, char **argv )
{
  int k;
  int room = 2;
  char *ep;
  static char *space = NULL;
  static int sroom = 0;
  for(k = arg0; k < argc; k++)
    room += strlen(argv[k]) + 1;
  if(room > sroom) {
    if(space) free(space);
    sroom = room + 50;
    space = malloc(sroom);
  }
  for(k = arg0, ep = space; k < argc; k++) {
    ep += sprintf(ep, "%s ", argv[k]);
  }
  if(ep > space) ep--;
  *ep = '\0';
  return space;
}
 
static enum SurfStyle getstyle( char *str )
{
  if(str == NULL || !strcmp(str, "solid") || !strncmp(str, "fill", 4)
		 || !strncmp(str, "poly", 4))
    return S_SOLID;
  if(!strncmp(str, "wire", 4) || !strncmp(str, "line", 4))
    return S_LINE;
  if(!strncmp(str, "plane", 5) || !strncmp(str, "ax", 2))
    return S_PLANE;
  if(!strncmp(str, "point", 5))
    return S_POINT;
  if(!strcmp(str, "off"))
    return S_OFF;
  msg("Unknown surface style \"%s\": want solid|line|plane|point|off", str);
  return S_SOLID;
}

/*
 * Xcen Ycen Zcen ellipsoid [-t txno] [-c cindex] [-s surfstyle] [-n nu[,nv]] [-r sizex[,y,z]] [0-or-9-or-16-numbers-tfm]
 */
void specks_read_ellipsoid( struct stuff *st, Point *pos, int argc, char **argv, char *comment ) {
  int i;
  struct ellipsoid e, *ep;
  float m3[3*3];
  float rxyz[3];

  memset(&e, 0, sizeof(e));
  e.cindex = -1;
  e.level = -1;
  e.pos = *pos;
  e.size.x[0] = e.size.x[1] = e.size.x[2] = 1;

  if(comment) {
    e.title = (comment[1] == ' ') ? comment+2 : comment+1;
  }

  for(i = 1; i+1 < argc; i += 2) {
    if(!strncmp(argv[i], "-c", 2)) {
	sscanf(argv[i+1], "%d", &e.cindex);
    } else if(!strncmp(argv[i], "-s", 2)) {
	e.style = getstyle(argv[i+1]);
    } else if(!strcmp(argv[i], "-r")) {
	int k = sscanf(argv[i+1], "%f%*c%f%*c%f", &e.size.x[0],&e.size.x[1],&e.size.x[2]);
	switch(k) {
	case 1: e.size.x[2] = e.size.x[1] = e.size.x[0]; break;
	case 3: break;
	default: msg("ellipsoid -r: expected 1 or 3 comma-separated semimajor axes not %s", argv[i+1]);
	}
    } else if(!strcmp(argv[i], "-n")) {
	int k = sscanf(argv[i+1], "%d%*c%d", &e.nu, &e.nv);
	switch(k) {
	case 1: e.nv = e.nu/2 + 1; break;
	case 2: break;
	default: msg("ellipsoid -n: expected nu or nu,nv but not %s", argv[i+1]);
	}
    } else if(!strcmp(argv[i], "-l")) {
	sscanf(argv[i+1], "%d", &e.level);
    } else {
	break;
    }
  }

  if(i == argc) {
    /* OK */
  } else if(i+9 == argc && 9==getfloats(m3, 9, i, argc, argv)) {
    memcpy(&e.ori.m[0*4+0], &m3[0], 3*sizeof(float));
    memcpy(&e.ori.m[1*4+0], &m3[3], 3*sizeof(float));
    memcpy(&e.ori.m[2*4+0], &m3[6], 3*sizeof(float));
    e.ori.m[3*4+3] = 1;
    e.hasori = 1;
  } else if(i+16 == argc && 16==getfloats(&e.ori.m[0], 16, i, argc, argv)) {
    e.hasori = 1;
  } else if(i+3 == argc && 3==getfloats(rxyz, 3, i, argc, argv)) {
    float aer[3];
    aer[0] = rxyz[1];
    aer[1] = rxyz[0];
    aer[2] = rxyz[2];
    xyzaer2tfm( &e.ori, NULL, aer );
    e.hasori = 1;
  } else {
    msg("ellipsoid: expected 0 or 3 (RxRyRz) or 9 or 16 numbers after options, not %s",
	rejoinargs(i, argc, argv));
    return;
  }

  /* OK, take it */
  ep = NewN( struct ellipsoid, 1 );
  *ep = e;
  if(e.title) ep->title = shmstrdup(e.title);
  ep->next = st->staticellipsoids;
  st->staticellipsoids = ep;
}

void specks_read_mesh( struct stuff *st, FILE *f, int argc, char **argv, char *buf )
{
  char *cp, *ep;
  int i, count, alloced, err = 0;
  int havetx = 0;
  Point pts[300], txs[300];
  struct mesh *m = NewN( struct mesh, 1 );
  int timestep = st->datatime;

  memset(m, 0, sizeof(*m));
  m->type = QUADMESH;
  m->cindex = -1;
  m->txno = -1;
  m->pts = pts; m->tx = txs;
  alloced = COUNT(pts);

  if(!strcmp(argv[0], "quadmesh") || !strcmp(argv[0], "mesh")) {
    m->type = QUADMESH;
  }

  for(i = 1; i+1 < argc; i += 2) {
    if(!strncmp(argv[i], "-time", 3)) {
	timestep = atoi(argv[i+1]);
    } else if(!strncmp(argv[i], "-t", 2)) {
	havetx = sscanf(argv[i+1], "%d", &m->txno);
    } else if(!strcmp(argv[i], "-static")) {
	timestep = -1;
	i--;
    } else if(!strncmp(argv[i], "-c", 2)) {
	sscanf(argv[i+1], "%d", &m->cindex);
    } else if(!strncmp(argv[i], "-s", 2)) {
	m->style = getstyle(argv[i+1]);
    } else {
	break;
    }
  }

  if(m->type == QUADMESH) {
    getline(f, buf);
    if(sscanf(buf, "%d%d", &m->nu, &m->nv) != 2
			|| m->nu <= 0 || m->nv <= 0) {
	msg("quadmesh: expected nu nv, got %s", buf);
	Free(m);
	return;
    }
    alloced = m->nverts = m->nu * m->nv;
    m->pts = NewN( Point, m->nverts );
    m->tx = havetx ? NewN( Point, m->nverts ) : NULL;
  }
    

  count = 0;
  while((cp = getline(f, buf)) != NULL) {
    int ngot;
    Point *tp;
#define BRA '{'
#define CKET '}'
    if(*cp == BRA) continue;
    if(*cp == CKET) break;

    if(count >= alloced) {
	if(m->type == QUADMESH) {
	    err = 1;
	    msg("Only expected %d*%d=%d vertices; what's %s",
		m->nu,m->nv, alloced, cp);
	    break;
	}
	/* otherwise, ask for more */
	/* XXX maybe later */
	break;
    }
	
    tp = &m->pts[count];
    for(ngot = 0; ngot < 3; ngot++, cp = ep) {
	tp->x[ngot] = strtod(cp, &ep);
	if(cp == ep) break;
    }
    if(havetx && ngot == 3) {
	tp = &m->tx[count];
	for(ngot = 0; ngot < 2; ngot++, cp = ep) {
	    tp->x[ngot] = strtod(cp, &ep);
	    if(cp == ep) break;
	}
	ngot++;
    }
    if(ngot != 3) {
	msg("Expected %d numbers per line; what's %s",
		havetx?5:3, buf); 
	err = 1;
	break;
    }
    count++;
  }
  if(m->type == QUADMESH && count < alloced) {
    err = 1;
    msg("Expected %d*%d = %d vertices, only got %d", m->nu,m->nv, alloced, count);
  }
  if(err) {
    if(m->pts != pts) Free(m->pts);
    if(m->tx != txs && m->tx != NULL) Free(m->tx);
    Free(m);
    return;
  }

  if(timestep >= 0) {
    if(timestep >= st->ntimes)
	specks_timespecksptr( st, st->curdata, timestep );
    m->next = st->meshes[st->curdata][timestep];
    st->meshes[st->curdata][timestep] = m;
  } else {
    m->next = st->staticmeshes;
    st->staticmeshes = m;
  }
}



void specks_read_waveobj( struct stuff *st, int argc, char **argv, char *line, char *infname )
{
  struct mesh *m = NewN( struct mesh, 1 );
  int lno;
  int i, f;
  int timestep = st->datatime;
  FILE *inf;
  char *fname, *fullname;
#define CHUNK 500
  int kpts, kfvs, kfv0, kfvn;
  int npts, nfvs, nfv0, nfvn;
  struct chain {
	void *data;
	int pos;
	struct chain *next;
  } *cpts, *cfvs, *cfv0, *cfvn;
  Point vpts[CHUNK];
  int vfvs[CHUNK], vfv0[CHUNK], vfvn[CHUNK];


  memset(m, 0, sizeof(*m));
  m->type = POLYMESH;
  m->cindex = -1;
  m->txno = -1;

  for(i = 1; i+1 < argc; i += 2) {
    if(!strncmp(argv[i], "-time", 2)) {
	timestep = atoi(argv[i+1]);
    } else if(!strncmp(argv[i], "-texture", 3) || !strncmp(argv[i], "-tx", 3)) {
	sscanf(argv[i+1], "%d", &m->txno);
    } else if(!strcmp(argv[i], "-static")) {
	timestep = -1;
	i--;
    } else if(!strncmp(argv[i], "-c", 2)) {
	sscanf(argv[i+1], "%d", &m->cindex);
    } else if(!strncmp(argv[i], "-s", 2)) {
	m->style = getstyle(argv[i+1]);
    } else {
	break;
    }
  }

  fname = argv[i];
  fullname = findfile( infname, fname );
  if(fullname == NULL || (inf = fopen(fullname, "r")) == NULL) {
    msg("waveobj: can't find .obj file %s", fname);
    return;
  }

#define VINIT(what) c##what = NULL, k##what = n##what = 0
#define VTOTAL(what) (k##what + n##what)
#define VNEXT(what) \
    if(++k##what >= CHUNK) { \
	struct chain *ct = (struct chain *)malloc(sizeof(*ct)); \
	ct->data = malloc(sizeof(v##what)); \
	memcpy(ct->data, v##what, sizeof(v##what)); \
	ct->pos = n##what; \
	ct->next = c##what; \
	c##what = ct; \
	k##what = 0; \
	n##what += CHUNK; \
    }
#define VSTUFF(dest, what) { \
	struct chain *ct, *cn; \
	memcpy( &dest[n##what], v##what, k##what * sizeof(v##what[0]) );  \
	for(ct = c##what; ct != NULL; ct = cn) {			  \
	    cn = ct->next;	\
	    memcpy( &dest[ct->pos], ct->data, CHUNK*sizeof(v##what[0]) ); \
	    free(ct->data);	\
	    free(ct);		\
	}			\
    }

  VINIT(pts);
  VINIT(fvs);
  VINIT(fv0);
  VINIT(fvn);
  lno = 0;
  while(fgets(line, LINEBUFSIZE, inf) != NULL) {
    char *word, *s = line;
    int slen;
    lno++;
    while(isspace(*s)) s++;
    if(*s == '#' || *s == '\0') continue;
    word = s++;
    while(*s != '\0' && !isspace(*s)) s++;
    slen = s - word;
    if(slen == 1 && word[0] == 'v') {
	Point *p = &vpts[kpts];
	char *ep;
	p->x[0] = strtod(s, &s);
	p->x[1] = strtod(s, &s);
	p->x[2] = strtod(s, &ep);
	if(s == ep) {
	    msg("waveobj: %s line %d: bad v line %s", fullname, lno, line);
	    continue;
	}
	VNEXT(pts);

    } else if(slen == 1 && word[0] == 'f') {
	char *ep;
	int myfv0 = VTOTAL( fvs );
	int myfvn = 0;

	for(;;) {
	    while(isspace(*s)) s++;
	    if(*s == '\0') break;
	    vfvs[kfvs] = strtol(s, &ep, 10) - 1;
	    if(s == ep) {
		msg("waveobj: %s line %d: bad f line %s", fullname, lno, line);
		myfvn = 0;
		break;
	    }
	    s = ep;
	    while(!isspace(*s)) s++;
	    VNEXT(fvs);
	    myfvn++;
	}
	vfv0[kfv0] = myfv0;
	vfvn[kfvn] = myfvn;
	if(myfvn >= 3) {
	    VNEXT(fv0);
	    VNEXT(fvn);
	}
    }
  }
  fclose(inf);
  m->pts = NewN( Point, VTOTAL(pts) );
  VSTUFF( m->pts, pts );
  m->fv0 = NewN( int, VTOTAL(fv0) );
  VSTUFF( m->fv0, fv0 );
  m->fvn = NewN( int, VTOTAL(fvn) );
  VSTUFF( m->fvn, fvn );
  m->fvs = NewN( int, VTOTAL(fvs) );
  VSTUFF( m->fvs, fvs );
  m->nfaces = VTOTAL(fvn);
  m->nfv = VTOTAL(fvs);
  m->nverts = VTOTAL(pts);
  for(i = m->nfv; --i >= 0; ) {
    if(m->fvs[i] < 0 || m->fvs[i] >= m->nverts) {
	fprintf(stderr, "%s: vert index %d out of range 1..%d\n",
		fullname, m->fvs[i]+1, m->nverts);
	m->fvs[i] = 0;
    }
  }
  m->fnorms = NewN( Point, m->nfaces );
  for(f = 0; f < m->nfaces; f++) {
    Point vab, vac, normal;
    int myfv0 = m->fv0[f];
    if(m->fvn[f] > 3 && m->fvs[myfv0] == m->fvs[myfv0+1])
	myfv0++;		/* degenerate quad? */
    vsub( &vab, &m->pts[ m->fvs[myfv0+1] ], &m->pts[ m->fvs[myfv0] ] );
    vsub( &vac, &m->pts[ m->fvs[myfv0+2] ], &m->pts[ m->fvs[myfv0] ] );
    vcross( &normal, &vab, &vac );
    vunit( &m->fnorms[f], &normal );
  }

  if(timestep >= 0) {
    if(timestep >= st->ntimes)
	specks_timespecksptr( st, st->curdata, timestep );
    m->next = st->meshes[st->curdata][timestep];
    st->meshes[st->curdata][timestep] = m;
  } else {
    m->next = st->staticmeshes;
    st->staticmeshes = m;
  }
}

void specks_read( struct stuff **stp, char *fname )
{
  FILE *f;
  char line[LINEBUFSIZE], oline[LINEBUFSIZE], *tcp;
  struct stuff *st = *stp;
  int maxfields = 0;

#define SPECKCHUNK 2001	/* should be bigger, but too many machines have tiny stack areas! */
#define MAXARGS 48
  int argc;
  char *argv[MAXARGS+1];
  struct speck speckbuf[SPECKCHUNK];
  struct speck *sp;
  struct speck s;
  struct specklist tsl;
  int i, nsp, maxnsp;
  int ignorefirst = 0;
  int lno = 0;
  char *comment;

  if(fname == NULL) return;

  tcp = NewA( char, strlen(fname)+1 );	/* fname's space might get reused */
  strcpy(tcp, fname);
  fname = tcp;

  if((f = fopen(fname, "rb")) == NULL) {
    msg("%s: can't open: %s", fname, strerror(errno));
    return;
  }

  tsl.bytesperspeck = (st->maxcomment+1 +
			(sizeof(s) - sizeof(s.title)) + 3) & ~3;
  s.rgba = 0;
  s.size = 1;
  nsp = 0;
  sp = speckbuf;
  maxnsp = sizeof(speckbuf) / tsl.bytesperspeck;

#define SPFLUSH() \
    if(nsp > 0) {					\
	addchunk( st, nsp, tsl.bytesperspeck,		\
		speckscale, speckbuf, NULL,		\
		maxfields >= MAXVAL ? tsl.bytesperspeck \
			: SMALLSPECKSIZE(maxfields) );	\
    }							\
    nsp = maxfields = 0;				\
    sp = speckbuf;

  line[sizeof(line)-1] = '\1';
  while( fgets(line, sizeof(line), f) != NULL ) {
    lno++;
    if(line[sizeof(line)-1] != 1) {
	if(line[sizeof(line)-2] != '\n') {
	    while((i = getc(f)) != EOF && i != '\n')
		;
	}
	line[sizeof(line)-1] = '\1';
	line[sizeof(line)-2] = '\0';
	msg("%s line %d: truncated (>%d chars long!)",
		fname, lno, sizeof(line)-2);
    }

    argc = tokenize(line, oline, MAXARGS, argv, &comment);

    while(argc > 0 && !strcmp(argv[0], "add")) {
	for(i = 1; i <= argc; i++) argv[i-1] = argv[i];
	argc--;
    }

    if(argc == 0)
	continue;


    if(nsp >= maxnsp || (isalnum(argv[0][0]) && !isdigit(argv[0][0]))) {
	SPFLUSH();
    }

    if(!strcmp(argv[0], "include") || !strcmp(argv[0], "read")) {
	float oldscale = speckscale;
	char *infname = argv[1];
	char *realfile = findfile( fname, infname );
	if(realfile == NULL) {
	    msg("%s: Can't find include-file %s", fname, infname);
	} else {
	    specks_read( &st, realfile );
	}
	speckscale = oldscale;

    } else if(!strcmp(argv[0], "object") && argc>1) {

	char *eq = strchr(argv[1], '=');

	i = 1;
	parti_object( argv[1], &st, 1 );
	if(eq) {
	    parti_set_alias( st, eq+1 );
	} else if(argc >= 3 && !strcmp(argv[2], "=")) {
	    /* object g3 = alias */
	    parti_set_alias( st, argv[3] );
	}

    } else if(parti_read(stp, argc, argv, fname)) {
	st = *stp;	/* In case st changed */

    } else if(!strcmp(argv[0], "sdb") && argc>1) {
	int tno = st->datatime;
	char *realfile;
	i = 1;
	if(argc>3 && !strcmp(argv[1], "-t")) {
	    if((tno = getfloat(argv[2], st->datatime)) < 0) {
		msg("sdb -t: expected timestepnumber(0-based), not %s", argv[2]);
		continue;
	    }
	    i = 3;
	}
	realfile = findfile( fname, argv[i] );
	if(realfile == NULL) {
	    msg("%s: sdb: can't find file %s", fname, argv[i]);
	} else {
	    specks_read_sdb( st, realfile, tno );
	}

    } else if(!strcmp(argv[0], "sdbvars")) {
	if(argc > 1) {
	    if(argv[1][strspn(argv[1], "mMcrogtxyzSn")] != '\0') {
		msg("sdbvars: pattern must contain only chars from: mMcrogtxyzSn, not %s",
			argv[1]);
	    } else {
		if(st->sdbvars) Free(st->sdbvars);
		st->sdbvars = shmstrdup(argv[1]);
	    }
	} else {
	    msg("sdbvars %s", st->sdbvars);
	}

    } else if(!strcmp(argv[0], "box") || !strcmp(argv[0], "boxes")) {
	Point cen, rad;
	struct AMRbox box;
	int k;

	int tno = -1;

	box.boxno = -1;
	box.level = 0;

	i = 1;
	while(i+2 < argc && argv[i][0] == '-') {
	    if(argv[i][1] == 'n' && sscanf(argv[i+1], "%d", &box.boxno)>0)
		i += 2;
	    else if(argv[i][1] == 'l' && sscanf(argv[i+1], "%d", &box.level)>0)
		i += 2;
	    else if(argv[i][1] == 't' && (tno = getfloat(argv[i+1], st->datatime)) >= 0)
		i += 2;
	    else
		break;
	}
	if(i+2 == argc &&
	     3==sscanf(argv[i], "%f%*c%f%*c%f", &cen.x[0],&cen.x[1],&cen.x[2]) &&
	     0<(k=sscanf(argv[i+1], "%f%*c%f%*c%f", &rad.x[0],&rad.x[1],&rad.x[2]))) {
	    if(k<3) rad.x[1] = rad.x[2] = rad.x[0];	/* if scalar radius */
	    vsub(&box.p0, &cen, &rad);
	    vadd(&box.p1, &cen, &rad);
	    specks_add_box( st, &box, tno );

	} else if(i+3 == argc &&
		    2==sscanf(argv[i], "%f%*c%f",&box.p0.x[0],&box.p1.x[0]) &&
		    2==sscanf(argv[i+1], "%f%*c%f",&box.p0.x[1],&box.p1.x[1]) &&
		    2==sscanf(argv[i+2], "%f%*c%f",&box.p0.x[2],&box.p1.x[2])) {

	    specks_add_box( st, &box, tno );

	} else if(i+6 == argc) {
	    k = getfloats( &box.p0.x[0], 3, i, argc, argv )
	      + getfloats( &box.p1.x[0], 3, i+3, argc, argv );
	    if(k==6)
		specks_add_box( st, &box, tno );
	    else
		msg("box: expected xmin ymin zmin  xmax ymax zmax  -or- xmin,xmax ymin,ymax zmin,zmax -or- xcen,ycen,zcen xrad,yrad,zrad");

	} else if(i+1 == argc) {
	    char *realfile = findfile( fname, argv[i] );
	    if(realfile == NULL) {
		msg("%s: boxes: can't find file %s", fname, argv[i]);
	    } else {
		specks_read_boxes( st, realfile, tno );
	    }
	} else {
	    msg("usage: box [-t timestep] AMRboxfile  -or-");
	    msg("  box [-t time] [-n boxno] [-l level] xcen,ycen,zcen  xradius,yradius,zradius  -or-");
	    msg("  box [options]  xmin ymin zmin  xmax ymax zmax");
	    msg(" options: -n <boxno>  box number, for \"gobox\" and \"boxlabel\" cmds (dflt -1)");
	    msg("   -l <boxlevel>      level-number (0..31) for \"showboxlevel\" cmds (dflt 0)");
	    msg("   -t <time>		timestep, if animated; default eternal,");
	    msg("			or (for animated AMRboxfile) start time 0");
	}

    } else if(!strncmp(argv[0], "annot", 5)) {
	static char ausage[] = "Usage: annot [-t timestep]  string...";
	int tno = st->datatime;

	i = 1;
	if(argc>1 && !strcmp(argv[1], "-t")) {
	    tno = getfloat(argv[2], -1);
	    i = 3;
	}
	if(i >= argc || tno < 0) {
	    msg(ausage);
	    continue;
	}
	specks_add_annotation( st, rejoinargs( i, argc, argv ), tno );

    } else if(!strcmp(argv[0], "cment") || !strcmp(argv[0], "textcment")
		|| !strcmp(argv[0], "boxcment")) {
	specks_parse_args( &st, argc, argv );

    } else if(!strcmp(argv[0], "size") && argc>1) {
	sscanf(argv[1], "%f", &s.size);

    } else if(!strcmp(argv[0], "scale") && argc>1) {
		/* "scale" in data file, not as VIRDIR command */
	float v = 0;
	sscanf(argv[1], "%f", &v);
	if(v != 0)
	    st->spacescale = speckscale = v;

    } else if(!strcmp(argv[0], "tfm") && argc>1) {
	int inv = 0;
	int mul = 0;
	int hpr = 0;
	int camparent = -1;
	int more, j, k;
	float scl;
	Matrix ot, t;
	Point xyz;
	float aer[3];
	char *key;
	i = 1;
	for(more = 1, key = argv[1]; *key != '\0' && more; key++) {
	  switch(*key) {
	  case '*': mul = 1; break;
	  case '/': inv = 1; break;
	  case 'h': case 'p': case 'r': hpr = 1; break;
	  case 'c': camparent = 1; i++; more = 0; break;
	  case 'w': camparent = 0; i++; more = 0; break;
	  case '\0': more = 0; i++; break;
	  default: more = 0; break;
	  }
	}

	if(camparent>=0)
	    parti_parent( st, camparent ? -1 : 0 );

	parti_geto2w( st, parti_object( NULL, &st, 0 ), &ot );

	k = getfloats( &t.m[0], 16, i, argc, argv );
	switch(k) {
	case 1:
		scl = t.m[0];
		t = Tidentity;
		t.m[0*4+0] = t.m[1*4+1] = t.m[2*4+2] = scl;
		break;
	case 6:
	case 7:		/* ignore fovy if included */
		xyz = *(Point *)&t.m[0];
		if(hpr) {
		    aer[0] = t.m[3], aer[1] = t.m[4], aer[2] = t.m[5];
		} else {
		    /* Note we permute: px py pz rx ry rz == px py pz e a r */
		    aer[1] = t.m[3], aer[0] = t.m[4], aer[2] = t.m[5];
		}
		xyzaer2tfm( &t, &xyz, aer );
		break;

	case 9:
		for(i = 2; i >= 0; i--)
		    for(j = 2; j >= 0; j--)
			t.m[i*4+j] = t.m[i*3+j];
		t.m[0*4+3] = t.m[1*4+3] = t.m[2*4+3] =
			t.m[3*4+0] = t.m[3*4+1] = t.m[3*4+2] = 0;
		t.m[3*4+3] = 1;
		break;

	case 16: break;
	case 0: if(camparent >= 0) continue;	/* allow "tfm c" or "tfm w" */
	default:
	    msg("Usage: tfm: expected 1 or 6 or 9 or 16 numbers not %s", line);
	    continue;
	}
	if(inv)
	    eucinv( &t, &t );
	if(mul)
	    mmmul( &t, &ot, &t );

	parti_seto2w( st, parti_object( NULL, &st, 0 ), &t );

    } else if((!strcmp(argv[0], "eval") || !strcmp(argv[0], "feed")
				   || !strcasecmp(argv[0], "VIRDIR"))
		&& argc > 1) {
#if CAVE
	if(0 == specks_parse_args( &st, argc-1, argv+1 )) {
	    /* grr, need to skip first word of line */
	    char *s = line;
	    while(isspace(*s)) s++;
	    while(!isspace(*s) && *s) s++;
	    VIDI_queue_commandstr( s );
	}

#else /* non-CAVE */
	specks_parse_args( &st, argc-1, argv+1 );

#endif /* non-CAVE */

    } else if(!strcmp(argv[0], "filepath")) {
	int k = 0;
	char **oldpath = getfiledirs();
	char *path[128], *dir;
	for(i = 1, k = 0; i < argc && k < COUNT(path)-1; i++) {
	    dir = strtok(argv[i], ":");
	    do {
		if(!strcmp(dir,"+")) {
		    int m = 0;
		    while(k<COUNT(path)-1 && oldpath && oldpath[m])
			path[k++] = oldpath[m++];
		} else {
		    path[k++] = dir;
		}
		dir = strtok(NULL, ":");
	    } while(dir && k < COUNT(path)-1);
	}
	path[k] = NULL;
	if(k > 0) filedirs(path);

    } else if(!strcmp(argv[0], "setenv") && argc==3 && NULL==strchr(argv[1],'=')) {
	char *c = NewN( char, strlen(argv[1]) + strlen(argv[2]) + 2 );
	sprintf(c, "%s=%s", argv[1], argv[2]);
	putenv(c);

    } else if(!strcmp(argv[0], "texture")) {
	int txno = -1;
	int txflags = TXF_SCLAMP | TXF_TCLAMP | TXF_ADD;
	int txapply = TXF_DECAL;
	int qual = 7;
	char *txfname, *key;
	i = 1;
	if(argc > 1 && sscanf(argv[1], "%d", &txno) > 0)
	    i = 2;
	while(i < argc && (key = argv[i])[0] == '-' || key[0] == '+') {
	    int tqual = (strchr(key,'m') ? 4:0) |
			(strchr(key,'l') ? 2:0) |
			(strchr(key,'n') ? 1:0);
	    if(key[0] == '-') qual &= ~tqual;
	    else qual |= tqual;
	    if(strchr(key,'a')) txflags |= TXF_ALPHA;
	    if(strchr(key,'i')) txflags |= TXF_INTENSITY;
	    if(strchr(key, 'A')) txflags |= TXF_ADD;
	    if(strchr(key, 'O')) txflags = (txflags & ~TXF_ADD) | TXF_OVER;
	    if(strchr(key, 'M')) txapply = TXF_MODULATE;
	    if(strchr(key, 'D')) txapply = TXF_DECAL;
	    if(strchr(key, 'B')) txapply = TXF_BLEND;
	    i++;
	}
	if((i < argc && txno < 0 && sscanf(argv[i++], "%d", &txno) <= 0) ||
	    (txfname = argv[i]) == NULL) {
		msg("Expected ``texture [-lmnaMDB] txno file.sgi'', got %s", line);
		msg(" opts: -l(inear) -m(ipmap) -n(earest) -i(intensity) -a(lpha) -A(dd) -O(ver(notAdd)) -M(odulate)|-D(ecal)|-B(lend))");
	} else {
	    txaddentry( &st->textures, &st->ntextures, fname, txno, txfname, txapply, txflags, qual );
	}

    } else if(!strcmp(argv[0], "polyorivar") && argc == 2) {
	if(1!=specks_set_byvariable( st, argv[1], &st->polyorivar0 ))
	    msg("polyorivar: unknown field %s", argv[1]);

    } else if(!strcmp(argv[0], "texturevar") && argc == 2) {
	if(1!=specks_set_byvariable( st, argv[1], &st->texturevar ))
	    msg("texturevar: unknown field %s", argv[1]);

    } else if(!strncmp(argv[0], "coord", 5) || !strncmp(argv[0], "altcoord", 8)) {
	float *t = &st->altcoord[0].w2coord.m[0];
	if(argc == 1) {
	    msg("coord %s  %g %g %g %g  %g %g %g %g  %g %g %g %g  %g %g %g %g",
		st->altcoord[0].name, t[0],t[1],t[2],t[3],
		t[4],t[5],t[6],t[7],
		t[8],t[9],t[10],t[11],
		t[12],t[13],t[14],t[15]);

	} else if(argc == 18) {
	    sprintf(st->altcoord[0].name, "%.9s", argv[1]);
	    i = getfloats( t, 16, 2, argc, argv );
	    if(i != 16) {
		msg("%s: expected 16 numbers; what's %s", argv[0],argv[i+2]);
		continue;
	    }
	} else {
	    msg("expected \"coord\" name  ... 16 world-to-coord tfm floats (GL order) ...");
	}

    } else if(!strncmp(argv[0], "dataset", 7) && argc == 3) {
	char name[12];
	int indexno;
	if(sscanf(argv[1], "%d", &indexno) <= 0
		|| sscanf(argv[2], "%11s", name) <= 0
		|| indexno < 0 || indexno >= MAXFILES) {
	    msg("%s: expected ``dataset <indexno> <datasetname>'' with 0<=indexno<=%d",
		fname, MAXFILES-1);
	    continue;
	}
	strncpyt(st->dataname[indexno], name, sizeof(st->dataname[indexno]));
	st->curdata = indexno;

    } else if(!strncmp(argv[0], "datavar", 7) && argc > 2) {
	int varno;
	struct valdesc vd;

	if(sscanf(argv[1], "%d", &varno)<=0
		|| sscanf(argv[2], "%19s", vd.name)<=0
		|| varno < 0 || varno >= MAXVAL) {
	    msg("%s: expected ``datavar <indexno> <variablename> [minval maxval]'' with 0<=indexno<=%d",
		line, MAXVAL-1);
	    continue;
	}
	strcpy(st->vdesc[st->curdata][varno].name, vd.name);
	if(argc == 5) {
	    st->vdesc[st->curdata][varno].min = atof(argv[3]);
	    st->vdesc[st->curdata][varno].max = atof(argv[4]);
	}

    } else if(!strcmp(argv[0], "datatime") && argc==2) {
	int newt = st->curtime;	/* so e.g. "datatime now" => apply to current data */
	sscanf(argv[1], "%d", &newt);
	if(sscanf(argv[1], "%d", &newt) <= 0 || newt < 0) {
	    msg("%s: datatime %s: timestep must be >= 0", fname, argv[1]);
	    continue;
	}
	if(newt == st->datatime)	/* no need to change anything */
	    continue;
	SPFLUSH();
	st->datatime = newt;

    } else if(!strcmp(argv[0], "mesh") || !strcmp(argv[0], "tstrip")
					|| !strcmp(argv[0], "tfan")) {
	specks_read_mesh(st, f, argc, argv, line);
	
    } else if(!strcmp(argv[0], "waveobj")) {
	specks_read_waveobj(st, argc, argv, line, fname);
	
	
    } else if(!strcmp(argv[0], "textcolor") && argc==2) {
	sscanf(argv[1], "%d", &s.rgba);

    } else if(!strcmp(argv[0], "maxcomment") && argc==2) {
	int newmax = st->maxcomment;
	sscanf(argv[1], "%d", &newmax);
	if(newmax != st->maxcomment) {
	    SPFLUSH();
	    st->maxcomment = newmax;
	    tsl.bytesperspeck =
		(st->maxcomment+1 + (sizeof(s) - sizeof(s.title)) + 3) & ~3;
	    maxnsp = sizeof(speckbuf) / tsl.bytesperspeck;
	}

    } else {
	struct valdesc *vdp;
	int k, m;

	k = getfloats( &s.p.x[0], 3, ignorefirst, argc, argv );
	if(k < 3) {
	    msg("Unrecognized datacmd: %s", line);
	    continue;
	}
	i = ignorefirst + k;
	vdp = &st->vdesc[st->curdata][0];
	k = getfloats( &s.val[0], COUNT(s.val), i, argc, argv );
	for(m = 0; m < k; m++) {
	    if(vdp->nsamples++ == 0) {
		vdp->min = vdp->max = s.val[m];
	    } else {
		if(vdp->min > s.val[m]) vdp->min = s.val[m];
		else if(vdp->max < s.val[m]) vdp->max = s.val[m];
	    }
	    vdp->sum += s.val[m];
	    vdp->mean = vdp->sum / vdp->nsamples;
	    vdp++;
	}

	if(maxfields < k) maxfields = k;
	m = i+k;
	s.title[0] = '\0';
	if(m < argc) {
	    if(!strcmp(argv[m], "text")) {
		s.size = 1;
		if(++m < argc-1 && !strcmp(argv[m], "-size")) {
		    sscanf(argv[m+1], "%f", &s.size);
		    m += 2;
		}
		addchunk( st, 1, SMALLSPECKSIZE(0),
			speckscale, &s,
			rejoinargs(m, argc, argv),
			SMALLSPECKSIZE(0) );
	    }
	    else if(!strcmp(argv[m], "ellipsoid")) {
		specks_read_ellipsoid( st, &s.p, argc-m, argv+m, comment );

	    } else {
		*sp = s;
		nsp++;
		sp = NextSpeck( speckbuf, &tsl, nsp );
	    }

	} else if(comment) {
	    char *title = comment + (comment[1] == ' ' ? 2 : 1);
	    *sp = s;
	    strncpyt(sp->title, title, st->maxcomment+1);
	    maxfields = MAXVAL+1;	/* "keep titles too" */
	    nsp++;
	    sp = NextSpeck( speckbuf, &tsl, nsp );

	} else {
	    *sp = s;
	    nsp++;
	    sp = NextSpeck( speckbuf, &tsl, nsp );
	}
    }
  }
  fclose(f);
  SPFLUSH();
  *stp = st;
}

/* Add a static (eternal) box to display list */
int specks_add_box( struct stuff *st, struct AMRbox *box, int timestep )
{
  int i;
  if(box->level >= 0 && st->boxlevels <= box->level)
    st->boxlevels = box->level+1;
  for(i = 0; i < st->staticboxroom && st->staticboxes[i].level >= 0; i++) {
    if(box->boxno != -1 && box->boxno == st->staticboxes[i].boxno) {
	st->staticboxes[i] = *box;
	return ~i;
    }
  }

  if(i >= st->staticboxroom-1) {
    st->staticboxroom = (st->staticboxroom>0) ? st->staticboxroom*2 : 12;
    st->staticboxes = RenewN( st->staticboxes, struct AMRbox, st->staticboxroom );
  }
  st->staticboxes[i] = *box;
  st->staticboxes[i+1].level = -1;
  return i;
}


void specks_read_boxes( struct stuff *st, char *fname, int timebase )
{
  FILE *f = fopen(fname, "rb");
  char line[1024];
  int ntimes = -1, maxbox = -1, curbox = 0, curtime = 0, level = -1, i;
  int lno = 0;
  int boxseq, boxno;
  struct AMRbox *boxes = NULL;
  struct AMRbox box;

  if(timebase < 0)	/* if unspecified time, start at first timestep */
    timebase = 0;

  if(f == NULL) {
    msg("Can't open AMRboxes file %s (as from hier2boxes.pl)", fname);
    return;
  }
  while(fgets(line, sizeof(line), f) != NULL) {
    lno++;
    boxno = -1;
    if(sscanf(line, "%f%f%f %f%f%f # %d",
		&box.p0.x[0],&box.p0.x[1],&box.p0.x[2],
		&box.p1.x[0],&box.p1.x[1],&box.p1.x[2], &boxno) >= 6) {
	for(i = 0; i < 3; i++) {
	    box.p0.x[i] *= st->spacescale;
	    box.p1.x[i] *= st->spacescale;
	}
	if(boxno != -1)
	    box.boxno = boxno;
	else
	    box.boxno = boxseq++;
	if(curbox < maxbox && boxes != NULL && level >= 0) {
	    boxes[curbox] = box;
	    curbox++;
	} else {
	    msg("%s line %d: Excess box (only expected %d at timestep %d)",
		fname, lno, maxbox, curtime);
	}
    }
    else if(sscanf(line, "AMRboxes %d timesteps", &ntimes) > 0 && ntimes >= 0) {
	int needtimes = ntimes + (timebase<0 ? 0 : timebase);
	if(st->boxtimes <= needtimes+1) {
	    needtimes += st->boxtimes + 15;	/* leave lots of extra room */
	    st->boxes = RenewN( st->boxes, struct AMRbox *, needtimes );
	    memset( &st->boxes[st->boxtimes], 0,
			(needtimes - st->boxtimes) * sizeof(struct AMRbox *) );
	    if(st->boxtimes < ntimes+timebase)
		st->boxtimes = ntimes+timebase;
	}
	boxes = NULL;
    }
    else if(sscanf(line, "timestep %d %d grids", &curtime, &maxbox) == 2) {
	boxseq = 1;
	if(ntimes < 0) {
	    msg(
"%s line %d: ``AMRboxes N timesteps'' must precede ``timestep'' lines!",
		    fname, lno);
	    curtime = -1;
	    boxes = NULL;
	    continue;
	} else if(curtime<0 || curtime>=ntimes || maxbox<0) {
	    msg("%s line %d: bad timestep number", fname, lno); 
	    curtime = -1;
	    boxes = NULL;
	    continue;
	}
	if((boxes = st->boxes[curtime+timebase]) != NULL) {
	    msg("%s line %d: Respecifying timestep %d",
		fname, lno, curtime);
	    Free( st->boxes[curtime+timebase] );
	}
	boxes = NewN( struct AMRbox, maxbox+1 );
	for(curbox = 0; curbox <= maxbox; curbox++)
	    boxes[curbox].level = -(maxbox+1);
	st->boxes[curtime+timebase] = boxes;

	/* Ensure that known span of time includes this timestep */
	(void) specks_timespecksptr( st, st->curdata, curtime+timebase );

	curbox = 0;
    }
    else if(sscanf(line, "level %d", &level) > 0) {
	if(boxes == NULL) {
	    msg("%s line %d: Must specify timestep before level",
		fname, lno);
	    level = -1;
	}
	if(st->boxlevels <= level)
	    st->boxlevels = level+1;
	box.level = level;
    }
  }
  fclose(f);

  /* st->boxlevelmask |= (1 << st->boxlevels) - 1;
   * Don't do this -- it turns on all boxlevels whenever we get a new box!
   */
}

static struct AMRbox *findboxno( struct AMRbox *box, int boxno, int *count, int *bmin, int *bmax ) {
  if(box == NULL) return NULL;
  while(box->level >= 0) {
    if(box->boxno == boxno)
	return box;
    if(*bmin > box->boxno) *bmin = box->boxno;
    if(*bmax < box->boxno) *bmax = box->boxno;
    ++*count;
    box++;
  } 
  return NULL;
}

int specks_gobox( struct stuff *st, int boxno, int argc, char *argv[] )
{
  Point pmid;
  int bmin = 1<<30, bmax = -1<<30;
  int count = 0;
  struct AMRbox *box = NULL;

  if(st->boxes && st->curtime < st->boxtimes)
    box = findboxno( st->boxes[st->curtime], boxno, &count, &bmin, &bmax );
  if(box == NULL)
    box = findboxno( st->staticboxes, boxno, &count, &bmin, &bmax );

  if(box == NULL) {
    if(count == 0)
	msg("No AMR boxes for timestep %d", st->curtime);
    else
	msg("%d AMR boxes (numbered %d..%d) for timestep %d, but no box number %d",
	    count, bmin, bmax, st->curtime, boxno);
    return 0;
  }

  vlerp( &pmid, .5*st->gscale, &box->p0, &box->p1 );
  set_interest_point( &pmid );

		/* round to 1 decimal */
#if CAVE
  {
    Point pmid, fwd, headv, pos, offset, newpos;
    static Point zvec = {0,0,1};
    float sz, scale;
    char cmd[128];
    sz = vdist( &box->p0, &box->p1 );
    sprintf(cmd, "%.1g", st->gscale * st->goboxscale * sz);
    scale = atof(cmd);
    CAVENavConvertVectorCAVEToWorld( zvec.x, fwd.x );
    CAVEGetPosition( CAVE_HEAD, headv.x );
    headv.x[2] -= 10.0;	/* a point in front of head, in CAVE coords */
    CAVENavConvertVectorCAVEToWorld( headv.x, offset.x );
    msg("scale %g caveunit %g offsetW %g %g %g", scale, vlength(&fwd), offset.x[0],offset.x[1],offset.x[2]);
    vsadd( &newpos, &pmid, -scale / vlength(&fwd), &offset );
    sprintf(cmd,
	sz > 0	? "jumpto %g %g %g  . . .  %g"
		: "jumpto %g %g %g",
	newpos.x[0], newpos.x[1], newpos.x[2], scale);
    VIDI_queue_commandstr( cmd );
  }
#endif

  return 1;
}

void specks_read_cmap( struct stuff *st, char *fname, int *ncmapp, struct cment **cmapp )
{
  int i;
  char *cp;
  char line[256];
  int k, count = 0;
  int *tcmap;
  struct cment *cm;
  float fr,fg,fb,fa;
  int big = 0;
  int lno = 0;
  int ncmap = 0;
  int cval;
  int csrc;

  FILE *f = fopen(fname, "rb");
  if(f == NULL) {
    msg("Can't open colormap file %s", fname);
    return;
  }
  tcmap = NULL;

  ncmap = 0;
  while(fgets(line, sizeof(line), f) != NULL) {
    lno++;
    cp = line;
   rescan:
    for( ; isspace(*cp); cp++)
	;
    if(*cp == '\0' || *cp == '#')
	continue;
    fa = 1.0;
    k = sscanf(cp, "%f%f%f", &fr,&fg,&fb/*,&fa*/);
    if(k == 1) {
	if(count == 0 && fr == (int)fr && fr > 0) {
	    count = fr;
	    if(count >= 1 && count < 1000000)		/* OK */
		continue;
	    msg("Unreasonable number of colormap entries claimed in header");
	    /* and fall into error case */
	} else {
	    while(isspace(*cp) || isdigit(*cp)) cp++;
	    if(*cp == ':') {
		ncmap = fr;
		if(ncmap < 0) {
		    ncmap = 0;		/* don't continue -- fall into error */
		} else if(ncmap >= count) {
		    msg("Colormap index %g exceeds size %d given in header",
			fr, count);	/* don't continue -- ditto */
		} else {
		    cp++;
		    goto rescan;		/* All's well */
		}
	    }
	}
    } else if(k >= 3) {
	if(fr > 2 || fg > 2 || fb > 2 || fa > 2)
	    big = 1;
	if(!big) {
	    fr *= 255; fg *= 255; fb *= 255; /*fa *= 255;*/
	}
	cval = PACKRGBA( (int)fr, (int)fg, (int)fb, 0 );
	if(tcmap == NULL) {
	    tcmap = NewA( int, count );
	    /* Fill unused entries with gray */
	    memset(tcmap, 0x80, count*sizeof(int));
	}
	tcmap[ncmap>=count ? count-1 : ncmap<0 ? 0 : ncmap] = cval;
	ncmap++;
	if(ncmap >= count)
	    break;
	continue;

    } else if(sscanf(cp, "=%d", &csrc) > 0) {
	if(csrc < 0 || csrc >= count || ncmap < 0 || ncmap >= count || tcmap == NULL) {
	    msg("Colormap index out of range: %d or %d isn't in 0..%d",
		ncmap, csrc, count-1);
	} else {
	    tcmap[ncmap++] = tcmap[csrc];
	    continue;
	}
	/* Fall through into error */
    }
    msg("Trouble reading colormap file %s: bad line %d: %s",
		fname, lno, line);
    fclose(f);
    return;
  }
  fclose(f);
  if(tcmap == NULL)
    return;

  *ncmapp = count;
  if(*ncmapp < 2) {
    *ncmapp = 2;
    tcmap[1] = tcmap[0];
  }
  *cmapp = cm = RenewN( *cmapp, struct cment, *ncmapp );
  k = *ncmapp;
  for(i = 0; i < k; i++) {
    cm[i].raw = tcmap[i];
    cm[i].cooked = specks_cookcment( st, tcmap[i] );
  }
}


#if CAVEMENU
void set_psize( float psize, MenuEnt *me, void *st ) {
   char str[20];
   ((struct stuff *)st)->psize = psize;
   sprintf(str, "pointsize %.3g", psize);
   menu_settitle( me, str );
}

	/* set_tknob() only used if PARTIMENU mentions "survey" */
void set_tknob( float val, MenuEnt *me, void *vtb ) {
  char str[128];
  struct stuff *st = ((struct boxleveler *)vtb)->st;
  int self = ((struct boxleveler *)vtb)->level;
  static int mod[3] = { 1, 4, 16 };
  static char *fmt[3] = {
	"StarMassEjFrac [%d/4]",
	"SNFeedback     [%d/4]",
	"StarEfficiency [%d/4]",
  };
  int range = 4, mine, rest, above;
  above = mod[self] * range;
  rest = st->curtime % mod[self] + (st->curtime / above) * above;
  mine = (st->curtime / mod[self]) % range;
  if(me->state == ME_PICKED || me->state == ME_HELD) {
    mine = val;
    if(mine < 0) mine = 0; else if(mine >= range) mine = range-1;
    clock_set_time( st->clk, rest + mine*mod[self] );
    clock_set_running( st->clk, 0 );
  }
  sprintf(str, fmt[self], mine+1);
  menu_settitle( me, str );
}

void set_step( float time0, MenuEnt *me, void *vst ) {
   char str[32];
   struct stuff *st = (struct stuff *)vst;
   if(me->state == ME_PICKED) {
	time0 += clock_fwd(st->clk);
	st->timeplay = 0;
	me->state = ME_HELD;
   }
   clock_set_time( st->clk, me->val = (int) time0 );
   /* NOTE we use the static values snapped by the framefunction.
    * st->curtime etc. might have changed since then.
    */
   sprintf(str, "step %2d of 0..%d", st->frame_time, st->ntimes - 1);
   if(st->fetching > 0)
	sprintf(str+strlen(str), " (loading %d)", st->fetchtime);
   menu_settitle( me, str );
}

void set_fwd( int fwd, MenuEnt *me, void *st ) {
   clock_set_fwd( ((struct stuff *)st)->clk, fwd );
   menu_settitle( me, fwd ? ">>> fwd >>>" : "<<< rev <<<" );
}

void set_viewall( int all, MenuEnt *me, void *vst ) {
  struct stuff *st = (struct stuff *)vst;
  if(all) {
    strcpy( st->vdcmd, "\002jump all" );
    menu_settitle( me, "view all" );
  }
  else {
    strcpy( st->vdcmd, "\002jump peak" );
    menu_settitle( me, "view peak" );
  }
}

float get_nav_scale(void)
{
  float nav[4][4];
  CAVENavGetMatrix( nav );
  return 1 / sqrtf( vdot( (Point *)&nav[0][0], (Point *)&nav[0][0] ) );
}

void set_scale( float logscale, MenuEnt *me, void *vst ) {
  struct stuff *st = (struct stuff *)vst;
  float scl = pow(10., logscale);
  char str[64];

  if(me->state == ME_PICKED || me->state == ME_HELD) {
    sprintf( st->vdcmd, "\002scale %g", scl);
  } else {
    scl = get_nav_scale();
    me->val = scl<=0 ? -6 : log10( scl );
  }
  sprintf( str, "scale %.4g", scl );
  menu_settitle( me, str );
}


void set_dataset( int which, MenuEnt *me, void *vst ) {
   char str[32];
   struct stuff *st = (struct stuff *)vst;
   if(which < 0 || which >= st->ndata || which >= MAXFILES) which = 0;
   st->curdata = which;
   sprintf(str, "viewing %s (%d)", st->dataname[which], which);
   menu_settitle( me, str );
}

void set_lumvar( int lumvar, MenuEnt *me, void *vst ) {
   char str[80];
   struct stuff *st = (struct stuff *)vst;
   struct valdesc *vd;
   if(lumvar < 0 || lumvar >= MAXVAL+1) lumvar = 0;
   st->sizedby = lumvar;
   vd = &st->vdesc[st->curdata][lumvar];
   if(vd->lmin == vd->lmax) vd->lmin = vd->min, vd->lmax = vd->max;
   sprintf(str, "lum by %s (%d) [%.3g..%.3g]", vd->name, lumvar, vd->lmin, vd->lmax);
   menu_settitle( me, str );
}

void set_colorvar( int colorvar, MenuEnt *me, void *vst ) {
   char str[80];
   struct stuff *st = (struct stuff *)vst;
   struct valdesc *vd;
   if(colorvar < 0 || colorvar >= MAXVAL+1) colorvar = 0;
   st->coloredby = colorvar;
   vd = &st->vdesc[st->curdata][colorvar];
   if(vd->cmin == vd->cmax) vd->cmin = vd->min, vd->cmax = vd->max;
   sprintf(str, "color by %s (%d) [%.3g..%.3g]", vd->name,
		colorvar, vd->cmin, vd->cmax);
   menu_settitle( me, str );
}

void set_slum( float slum, MenuEnt *me, void *vst ) {
   char str[32];
   struct stuff *st = (struct stuff *)vst;
   st->vdesc[st->curdata][st->sizedby].lum = slum;
   sprintf(str, slum == 0 ? "scale-lum <unset>" : "scale-lum %.2g", slum);
   menu_settitle( me, str );
}

void set_speed( float speed, MenuEnt *me, void *vst ) {
   char str[32];
   struct stuff *st = (struct stuff *)vst;
   specks_set_speed( st, speed );
   sprintf(str, "speed %.1f steps/anim sec", speed);
   menu_settitle( me, str );
}

void set_fspeed( float speed, MenuEnt *me, void *vst ) {
   char str[64];
   struct stuff *st = (struct stuff *)vst;
   specks_set_fspeed( st, speed );
   if(me->state == ME_PICKED) {
	clock_set_running( st->clk, !clock_running(st->clk) );
	me->state = ME_HELD;	/* only once */
   }
   sprintf(str, "fspeed %.1f steps/real sec (%s)", speed,
			clock_running(st->clk) ? "running" : "stopped");
   menu_settitle( me, str );
}

void set_seedata( int which, MenuEnt *me, void *vst ) {
   struct stuff *st = (struct stuff *)vst;
   char str[64];
   if(which >= st->ndata) which = st->ndata - 1;
   if(which < 0) which = 0;
   st->curdata = which;
   sprintf(str, "seedata %.24s (%d)", st->dataname[which], which);
   menu_settitle( me, str );
}

void set_every( float every, MenuEnt *me, void *vst ) {
   char str[32];
   struct stuff *st = (struct stuff *)vst;
   if(every < 1) every = 1;
   st->subsample = me->val = every;
   sprintf(str, "every %dth p'cle (of %d)", (int)every, specks_count(st->sl));
   menu_settitle( me, str );
}

void set_menu( int on, MenuEnt *me, void *vst ) {
   struct stuff *st = (struct stuff *)vst;
   int empty = st->ntimes == 0 && st->sl == NULL;
   if(me->state == ME_PICKED)
	st->hidemenu = !st->hidemenu;
   menu_sethidden( pmenu, st->hidemenu || empty );
   menu_sethidden( stubmenu, st->hidemenu<=0 || empty );
}

void set_allboxes( int on, MenuEnt *me, void *vst ) {
   struct stuff *st = (struct stuff *)vst;
   st->useboxes = on;
   menu_settitle( me, on==2 ? "Boxes ONLY" :
			on ? "Boxes ON" : "Boxes off" );
}

void set_boxlevel( int on, MenuEnt *me, void *vblr ) {
   struct boxleveler *blr = (struct boxleveler *)vblr;
   struct stuff *st = blr->st;
   int bit = 1 << blr->level;
   char str[32];

   if(blr->level >= st->boxlevels) {
	menu_settitle(me, "" );
   } else {
	if(me->state == ME_PICKED)
	    st->boxlevelmask ^= bit;
	sprintf(str, "Level %d %s", blr->level, 
		st->boxlevelmask & bit ? "ON" : "off" );
	menu_settitle( me, str );
   }
}

void
set_point( int on, MenuEnt *me, void *st )
{
  struct stuff *stuff = (struct stuff *)st;
  stuff->usepoint = (on<0) ? !stuff->usepoint : on;
  menu_settitle( me, stuff->usepoint ? "Point On" : "Point Off" );
}

void
set_poly( int on, MenuEnt *me, void *st )
{
  struct stuff *stuff = (struct stuff *)st;
  stuff->usepoly = (on<0) ? !stuff->usepoly : on;
  menu_settitle( me, stuff->usepoly ? "Poly On" : "Poly Off" );
}

void
set_label( int on, MenuEnt *me, void *st )
{
  struct stuff *stuff = (struct stuff *)st;
  stuff->usetext = (on<0) ? !stuff->usetext : on;
  menu_settitle( me, stuff->usetext ? "Labels On" : "Labels Off" );
}

void
set_alpha( float alpha, MenuEnt *me, void *st )
{
   char str[20];
   ((struct stuff *)st)->alpha = alpha;
   sprintf(str, "Alpha %.2f", alpha);
   menu_settitle( me, str );
}

void
set_fog( float fog, MenuEnt *me, void *st )
{
   char str[20];
   ((struct stuff *)st)->fog = fog;
   sprintf(str, "Fog %.2f (not yet)", fog);
   menu_settitle( me, str );
}

void
set_polysize( float polysize, MenuEnt *me, void *st )
{
   char str[20];
   ((struct stuff *)st)->polysize = polysize;
   sprintf(str, "Spotsize %.2f", polysize);
   menu_settitle( me, str );
}
#endif /*CAVEMENU*/



int specks_set_byvariable( struct stuff *st, char *str, int *val )
{
  int i;
  char *ep;
  int best = -1;
  if(str == NULL) return 0;

  if(!strcasecmp( str, "const" ) || !strcasecmp( str, "constant" )
				 || !strcasecmp( str, "rgb" )) {
    *val = CONSTVAL;
    return -1;
  }
	
  for(i = 0; i < MAXVAL; i++) {
    if(strncasecmp( str, st->vdesc[st->curdata][i].name, strlen(str) ) == 0) {
	best = i;
	if(!strcmp( str, st->vdesc[st->curdata][i].name ))
	    break;
    }
  }
  if(best >= 0) {
    *val = best;
    return 1;
  }
  i = strtol(str, &ep, 0);
  if(ep == str || i < 0 || i > MAXVAL || (*ep != '\0' && *ep != '('))
    return 0;
  *val = i;
  return 1;
}

static char *putcoords( char *buf, Point *pos, Matrix *T,
		int isvec, char *cartfmt,
		char *lonlatfmt, char *hmdmfmt )
{
  Point p;
  char *cp = buf;
  float lat, lon, r;
  if(T == NULL) T = &Tidentity;
  buf[0] = '\0';
  if(isvec)
    vtfmvector( &p, pos, T );
  else vtfmpoint( &p, pos, T );
  if(cartfmt)
    cp += sprintf(cp, cartfmt, p.x[0],p.x[1],p.x[2]);
  lon = atan2(p.x[1], p.x[0]) * 180/M_PI;
  if(lon < 0) lon += 360;
  lat = atan2(p.x[2], hypot(p.x[1],p.x[0])) * 180/M_PI;
  r = vlength(&p);
  if(lonlatfmt)
    cp += sprintf(cp, lonlatfmt, lon, lat, r);
  if(hmdmfmt) {
    char *sign = (lat < 0) ? "-" : "+";
    lat = fabs(lat);
    lon /= 15;
    cp += sprintf(cp, hmdmfmt, (int)lon, 60. * (lon - (int)lon),
				sign, (int)lat, 60. * (lat - (int)lat),
				r);
  }
  return buf;
}

static char *whereis(struct stuff *st, char *buf, Point *pos, int isvec)
{
  putcoords(buf, pos, NULL, isvec, "%.4f %.4f %.4f", NULL, NULL);
  if(strstr(st->altcoord[0].name, "2000")
	|| strstr(st->altcoord[0].name, "1950")
	|| strstr(st->altcoord[0].name, "eq")) {

    sprintf(buf + strlen(buf), "; %.9s: ", st->altcoord[0].name);
    putcoords(buf+strlen(buf), pos, &st->altcoord[0].w2coord, isvec,
		NULL, NULL, "%02d:%02.0f %s%02d:%02.0f %g");

  } else if(st->altcoord[0].name[0] != '\0') {
    sprintf(buf + strlen(buf), "; %.9s: ", st->altcoord[0].name);
    putcoords(buf+strlen(buf), pos, &st->altcoord[0].w2coord, isvec,
		NULL, "%.4f %.4f %g", NULL);
  }
  return buf;
}


static Point zero = {0,0,0}, forward = {0,0,-1};

#if CAVE

void set_where(int which, MenuEnt *me, void *vst) {
  struct stuff *st = (struct stuff *)vst;
  char buf[180];
  Point pos;
  Matrix cam2w;
  switch(which) {
  case 0:
    CAVEGetPosition( CAVE_HEAD_NAV, pos.x );
    sprintf(buf, "head at ");
    whereis(st, buf+strlen(buf), &pos, 0);
    break;

  case 1:
    VD_get_cam2world_matrix( cam2w.m );
    vtfmpoint( &pos, &zero, &cam2w );
    sprintf(buf, "camera at ");
    whereis(st, buf+strlen(buf), &pos, 0);
    break;

  case 2:
    VD_get_cam2world_matrix( cam2w.m );
    vtfmvector( &pos, &forward, &cam2w );
    sprintf(buf, "cam looks ");
    whereis(st, buf+strlen(buf), &pos, 1);
    break;

  case 3:
    VD_get_interest( pos.x );
    sprintf(buf, "interest at ");
    whereis(st, buf+strlen(buf), &pos, 0);
    break;
  default:
    return;
  }
  menu_settitle( me, buf );
}
#endif /*CAVE*/

static void tellwhere(struct stuff *st)
{
    Point pos, fwd;
    float aer[3], scl;
    Matrix cam2w;
    char buf[180];

#ifdef CAVE
    CAVENavConvertCAVEToWorld( zero.x, pos.x );
    msg("cave at %s", whereis(st, buf, &pos, 0));
    CAVEGetPosition( CAVE_HEAD_NAV, pos.x );
    msg("head at %s", whereis(st, buf, &pos, 0));
    CAVEGetPosition( CAVE_WAND_NAV, pos.x );
    msg("wand at %s", whereis(st, buf, &pos, 0));
    CAVEGetVector( CAVE_WAND_FRONT_NAV, fwd.x );
    VD_get_cam2world_matrix( cam2w.m );
#else
    parti_getc2w( &cam2w );
#endif

    scl = tfm2xyzaer( &pos, aer, &cam2w );
    vtfmvector( &fwd, &forward, &cam2w );
    msg("camera at %s", whereis(st, buf, &pos, 0));
    msg("looking to %s", whereis(st, buf, &fwd, 1));
    msg("jump %g %g %g  %g %g %g  %g",
	pos.x[0],pos.x[1],pos.x[2],
	aer[1],aer[0],aer[2],
	scl);
    msg("c2w: %g %g %g %g  %g %g %g %g  %g %g %g %g  %g %g %g %g",
	cam2w.m[0], cam2w.m[1], cam2w.m[2], cam2w.m[3],
	cam2w.m[4], cam2w.m[5], cam2w.m[6], cam2w.m[7],
	cam2w.m[8], cam2w.m[9], cam2w.m[10], cam2w.m[11],
	cam2w.m[12], cam2w.m[13], cam2w.m[14], cam2w.m[15]);
}


int getbool( char *str, int defval ) {
  int v;
  char *ep;
  if(str == NULL) return defval;

  if(!strcasecmp(str, "on")) return 1;
  if(!strcasecmp(str, "off")) return 0;
  if(!strcasecmp(str, "toggle")) return !defval;
  if(!strcasecmp(str, "all")) return -1;
  v = strtol(str, &ep, 0);
  if(str == ep) return defval;
  return v;
}

double getfloat( char *str, double defval ) {
  double v;
  char *ep;
  int prefix = 0;
  if(str == NULL) return defval;
  if(str[0] == '-' && (str[1] == '=' || str[1] == '-'))	/* -=, -- */
    prefix = *str++;
  if(str[0] == '*' || str[0] == '+' || str[0] == '/' || str[0] == 'x')
    prefix = *str++;
  if(str[0] == '=')
    str++;
  v = strtod(str, &ep);
  if(ep == str) {
    v = defval;
  } else {
    switch(prefix) {
    case '-': v = defval - v; break;
    case '+': v = defval + v; break;
    case '*': v = defval * v; break;
    case '/': v = (v != 0) ? defval / v : 0; break;
    }  /* default: just use v */
  }
  return v;
}

int getfloats( float *v, int nfloats, int arg0, int argc, char **argv ) {
  int i;
  char *ep;
  for(i = 0; i < nfloats && arg0+i < argc; i++) {
    float tv = strtod( argv[arg0+i], &ep );
    if(ep == argv[arg0+i])
	break;
    v[i] = tv;
  }
  return i;
}

void editcmap( struct stuff *st, int ncmap, struct cment *cmap, char *range,
		char *whose, char *colorstr )
{
    int i, min, max;
    char *cp;
    struct cment cm;

    cp = colorstr;
    while(isspace(*cp)) cp++;
    if(*cp == ':' || *cp == '=') {
	char junk;
	do cp++; while(*cp == '=');
	if(sscanf(cp, "%d%c", &i, &junk) != 1) {
	    msg("%s: expected entrynumber(s) := oldindex, not %s", whose, colorstr);
	    return;
	}
	if(i < 0 || i >= ncmap) {
	    msg("%s: clamping index %d to 0..%d", whose, i, ncmap-1);
	    i = (i < 0) ? 0 : ncmap-1;
	}
	cm = cmap[i];
    } else {
	float r, g, b;
	if(sscanf(cp, "%f%f%f", &r,&g,&b) != 3) {
	    msg("%s: expected entrynumber(s) r g b, not %s", whose, colorstr);
	    return;
	}
	cm.raw = PACKRGBA( (int)(r*255), (int)(g*255), (int)(b*255), 0 );
	cm.cooked = specks_cookcment( st, cm.raw );
    }

    for(cp = range; ; cp++) {
	while(isspace(*cp) || *cp == ',') cp++;
	if((i = sscanf(cp, "%d-%d", &min, &max)) > 0) {
	    if(i == 1) max = min;
	} else if((i = sscanf(cp, ">%d", &min)) > 0) {
	    max = ncmap-1;
	} else if((i = sscanf(cp, "<%d", &max)) > 0) {
	    min = 0;
	}
	if(i > 0) {
	    if(min < 0) min = 0;
	    if(max >= ncmap) max = ncmap-1;
	    for(i = min; i <= max; i++)
		cmap[i] = cm;
	}
	while(*cp != ',' && !isspace(*cp)) {
	    if(*cp == '\0') return;
	    cp++;
	}
    }
}

int
specks_parse_args( struct stuff **stp, int argc, char *argv[] )
{
  int i;
  struct stuff *st = *stp;

#if !CAVEMENU
  if(parti_parse_args( stp, argc, argv, NULL ) > 0)
    return 1;
#endif

  while( argc>0 &&
	(!strncmp( argv[0], "specks", 4 ) ||
	 !strcmp( argv[0], "feed" ) ||
	 !strcmp( argv[0], "eval" )) ) {
    argc--, argv++;
    /* VD_select_menu( specks_menuindex ); */
  }

  if(argc <= 0)
    return 0;

  if( st->dyn.enabled && st->dyn.ctlcmd &&
		(*st->dyn.ctlcmd)( &st->dyn, st, argc, argv ) ) {
    /* OK, dyn command handled it */

  } else if(!strcmp( argv[0], "?" ) || !strcmp( argv[0], "help" )) {
    static char *help1[] = {
"specks commands:",
" speed   data-steps per VirDir second",
" step N  -or-  step +N  -or-  step -N  Go to data step N, or step fwd/back",
" trange on|off|MIN MAX [WRAP]	limit range of datastep times",
" run				toggle auto-play (run/step)",
" color VARNO-or-NAME		color particles by VARNO'th variable (0..%d)",
" color const R G B		set all particles to be that color",
" lum   VARNO-or-NAME		tie particle size/luminosity to VARNOth var",
" lum   const LUM		set all particles to be brightness LUM",
" slum  SCALEFACTOR		scale particle brightness by SCALEFACTOR",
" psize SIZE			scale particle brightness by SIZE * SCALEFACTOR",
" depthsort			sort polygons by depth",
" see   DATASETNO-or-NAME	show that dataset (e.g. \"seedata 0\" or \"seedata gas\")",
" read  [-t time] DATAFILENAME	read data file (e.g. to add new specks)",
" ieee  [-t time] IEEEIOFILE	read IEEEIO file (starting at given timestep)",
" sdb   [-t time] SDBFILE	read .sdb star-data file",
" annot [-t time] string	set annotation string (for given timestep)",
" add  DATAFILECOMMAND		enter a single datafile command (ditto)",
" every N			subsample: show every Nth particle",
" bound				show bounds (coordinate range of all particles)",
" clipbox {on | off | X0,X1 Y0,Y1 Z0,Z1 | CENX,Y,Z RADX,Y,Z | X0 Y0 Z0  X1 Y1 Z1} clipping region",
" add box [-n boxno] [-l level] CENX,Y,Z RX,RY,RZ | X0 Y0 Z0 X1 Y1 Z1  marker-box",
" boxlabels                     show box numbers",
" boxaxes                       show R/G/B axes from X0,Y0,Z0 box corner"
#if CAVEMENU
" boxes {off|on|only}		hide/show all AMR boxes",
" {hide|show} LEVELNO ...	hide/show AMR boxes of those levels (or \"all\")",
" {point|polygon|texture} {on|off}",
" fmenu HEIGHT  -or-  fmenu XPOS YPOS  -or- fmenu wall WALLNO",
#else
" readpath  FILENAME.wf		read Wavefront camera path (from virdir \"wfout\")",
" play  SPEED[f]		play path (at SPEED times normal speed)",
"			(with \"f\" suffix, play every SPEEDth frame)",
" frame FRAMENO			go to Nth frame",
" focal FOCALLEN		focal length (determines fly/tran speed)",
" clip  NEAR FAR		clipping distances",
" jump  X Y Z [RX RY RZ]	put viewpoint there",
" center X Y Z			set center of rotation for orbit/rotate",
" censize RADIUS		size of center marker",
" snapset  filestem [frameno]	set snapshot parameters",
" snapshot [frameno]		take snapshot [uses convert(1)]",
" kira {node|ring|size|scale|span|track}  starlab controls; try \"kira ?\"",
#endif
    };

    for(i = 0; i < COUNT(help1); i++)
	msg(help1[i], MAXVAL-1);

    if(st->dyn.enabled && st->dyn.help)
	(*st->dyn.help)(&st->dyn, st, 0);
  

  } else if(!strcmp( argv[0], "read" )) {
	if(argc > 1) {
	    char *foundfile = findfile( NULL, argv[1] );
	    if(foundfile == NULL)
		msg("read: can't find file %s", argv[1]);
	    else
		specks_read( &st, foundfile );
	}

  } else if(!strcmp( argv[0], "include" )) {
#ifdef NOTYET
#endif

  } else if( !strcmp(argv[0], "on") || !strcmp(argv[0], "off")
	    || !strcmp(argv[0], "enable") || !strcmp(argv[0], "disable") ) {

	st->useme = argc>1 ? getbool(argv[1], st->useme) : (argv[0][1]=='n');
	msg(st->useme ? "enabled" : "disabled");
	
  } else if(!strcmp(argv[0], "add")) {
	int k, io[2];
	FILE *tf;
	char fdname[64+L_tmpnam];

#ifdef WIN32
	tmpnam(fdname);
	tf = fopen(fdname, "w");
#else /* unix */
	pipe(io);
	sprintf(fdname, "/dev/fd/%d", io[0]);
	tf = fdopen(io[1], "w");
#endif
	if(tf == NULL) {
	    fprintf(stderr, "Yeow: can't make temp file?\n");
	} else {
	    for(k = 1; k < argc; k++)
		fprintf(tf, "%s ", argv[k]);
	    fprintf(tf, "\n");
	    fclose(tf);
	    specks_read( &st, fdname );
#ifdef WIN32
	    unlink(fdname);
#endif
	}

#ifndef WIN32
	close(io[0]);
#endif

  } else if(!strcmp( argv[0], "async" ) ||
	    !strcmp( argv[0], "|" )) {
	specks_add_async( st, rejoinargs( 1, argc, argv ),
				argv[0][0] == '|' );

  } else if(!strcmp( argv[0], "update" )) {
	parti_update();

  } else if(!strcmp( argv[0], "hist" )) {
	register struct specklist *sl;
	register struct speck *sp;
	int nspecks, nclipped, nthreshed, nlow, nhigh, nundefined;
	int clipping = (st->clipbox.level > 0);
	int threshing = st->usethresh & P_USETHRESH;
	int nbuckets = 11;
	int dolog = 0;
	int *bucket;
	float v, vmin, vmax, vrange;
	int histvar;
	int i, k, bno;
	struct valdesc *vd;

	for(i=1,k=1; i+1 < argc; k++) {
	    int yes = argv[i][0] == '-';
	    if(argv[i][0] != '-' && argv[i][1] != '+') break;
	    switch(argv[i][k]) {
		case 't': threshing = yes ? THRESHBIT : 0; break;
		case 'c':
		case 'b': clipping = yes; break;
		case 'n':
		    sscanf(argv[i][++k] ? &argv[i][k] : argv[++i],
			"%d", &nbuckets);
		    i++; k=0;
		    break;
		case 'l': dolog = yes; break;
		case '\0': i++; k=0;
	    }
	}

	if(!specks_set_byvariable( st, argv[i], &histvar )) {
	    msg("hist %s: expected name/index of data variable", argv[i]);
	    return 1;
	}

	vd = &st->vdesc[st->curdata][histvar];
	vmin = vd->min;
	vmax = vd->max;

	if(i+1<argc) sscanf(argv[i+1], "%f", &vmin);
	if(i+2<argc) sscanf(argv[i+2], "%f", &vmax);

	if(vmax < vmin)
	    v = vmax, vmax = vmin, vmin = v;

	if(dolog && (vmin <= 0 || vmax <= 0)) {
	    msg("hist: can't take logs (-l) if range includes zero!");
	    dolog = 0;
	}
	if(dolog) {
	    vmin = log(vmin);
	    vmax = log(vmax);
	}
	vrange = (nbuckets-1) / ((vmax>vmin) ? vmax - vmin : 1);

	if(nbuckets<=0 || nbuckets>20000) {
	    msg("hist -n %d: Incredible number of histogram buckets", nbuckets);
	    return -1;
	}

	bucket = NewA(int, nbuckets);
	memset(bucket, 0, nbuckets*sizeof(int));

	nspecks = nclipped = nthreshed = nundefined = nlow = nhigh = 0;
	for(sl = st->sl; sl != NULL; sl = sl->next) {
	    if((sp = sl->specks) == NULL || sl->text != NULL)
		continue;
	    nspecks += sl->nspecks;
	    if(sl->bytesperspeck < SMALLSPECKSIZE(histvar+1)) {
		nundefined += sl->nspecks;
		continue;
	    }
	    for(i = sl->nspecks; --i >= 0; sp = NextSpeck( sp, sl, 1 )) {
		if(clipping &&
		  (sp->p.x[0] < st->clipbox.p0.x[0] ||
		   sp->p.x[0] > st->clipbox.p1.x[0] ||
		   sp->p.x[1] < st->clipbox.p0.x[1] ||
		   sp->p.x[1] > st->clipbox.p1.x[1] ||
		   sp->p.x[2] < st->clipbox.p0.x[2] ||
		   sp->p.x[2] > st->clipbox.p1.x[2])) {
			nclipped++;
			continue;
		}
		if(threshing & sp->rgba) {
			nthreshed++;
			continue;
		}
		v = sp->val[histvar];
		if(dolog) {
		    if(v <= 0) {
			nundefined++;
			continue;
		    }
		    v = log(v);
		}
		bno = (int) ((v - vmin)*vrange);
		if(bno < 0) nlow++;
		else if(bno >= nbuckets) nhigh++;
		else bucket[bno]++;
	    }
	}
	if(nspecks == 0) {
	    msg("No specks loaded yet");
	} else {
	    msg("hist -n %d %s%s%s%d(%s) %g %g => ",
		nbuckets, dolog?"-l ":"", clipping?"-c ":"", threshing?"-t ":"",
		histvar, vd->name,
		dolog ? exp(vmin) : vmin,
		dolog ? exp(vmax) : vmax);
	    msg("Total %d, %d < min, %d > max, %d undefined, %d clipped, %d threshed",
		nspecks, nlow, nhigh, nundefined, nclipped, nthreshed);
	    k = nlow;
	    msg("%d\t< %g", nlow, dolog ? exp(vmin) : vmin);
	    for(i = 0; i < nbuckets; i++) {
		v = vmin + ( (vrange>0) ? i / vrange : 0 );
		msg("%d\t>= %g", bucket[i], dolog ? exp(v) : v);
	    }
	    msg("%d\t> %g", nhigh, dolog ? exp(vmax) : vmax);
	}


  } else if(!strcmp( argv[0], "bound" )) {
	register struct specklist *sl;
	register struct speck *sp;
	Point xmin,xmax, mean, mid, radius;
	int minmaxk = MAXVAL+1, maxmaxk = 0;
	int nspecks = 0;

	xmin.x[0] = xmin.x[1] = xmin.x[2] = 1e38;
	xmax.x[0] = xmax.x[1] = xmax.x[2] = -1e38;
	mean.x[0] = mean.x[1] = mean.x[2] = 0;
	for(sl = st->sl; sl != NULL; sl = sl->next) {
	    int i, maxk;
	    if((sp = sl->specks) == NULL)
		continue;
	    for(maxk=0; maxk<MAXVAL && SMALLSPECKSIZE(maxk)<sl->bytesperspeck; maxk++)
		;
	    if(minmaxk > maxk) minmaxk = maxk;
	    if(maxmaxk < maxk) maxmaxk = maxk;
	    for(i = sl->nspecks; --i >= 0; sp = NextSpeck( sp, sl, 1 )) {
		if(xmin.x[0] > sp->p.x[0]) xmin.x[0] = sp->p.x[0];
		if(xmax.x[0] < sp->p.x[0]) xmax.x[0] = sp->p.x[0];
		mean.x[0] += sp->p.x[0];
		if(xmin.x[1] > sp->p.x[1]) xmin.x[1] = sp->p.x[1];
		if(xmax.x[1] < sp->p.x[1]) xmax.x[1] = sp->p.x[1];
		mean.x[1] += sp->p.x[1];
		if(xmin.x[2] > sp->p.x[2]) xmin.x[2] = sp->p.x[2];
		if(xmax.x[2] < sp->p.x[2]) xmax.x[2] = sp->p.x[2];
		mean.x[2] += sp->p.x[2];
	    }
	    nspecks += sl->nspecks;
	}
	if(nspecks == 0) {
	    msg("No specks loaded yet");
	} else {
	
	    vscale( &mean, 1.0/nspecks, &mean );
	    vcomb( &mid, .5,&xmin, .5,&xmax );
	    vcomb( &radius, .5,&xmax, -.5,&xmin );
	    msg( "%d specks in range %g %g %g .. %g %g %g",
		nspecks, xmin.x[0],xmin.x[1],xmin.x[2],
		xmax.x[0],xmax.x[1],xmax.x[2]);
	    msg( "midbbox %g %g %g  boxradius %g %g %g",
		mid.x[0],mid.x[1],mid.x[2],
		radius.x[0],radius.x[1],radius.x[2]);
	    msg( "mean %g %g %g", mean.x[0],mean.x[1],mean.x[2] );
	}

  } else if(!strcmp( argv[0], "fspeed" )) {
	if(argc>1) {
	    specks_set_fspeed( st, getfloat(argv[1], st->fspeed) );
	    st->playnext = 0.0;
	}
	msg("fspeed %g steps per real-time second", st->fspeed);

  } else if(!strcmp( argv[0], "speed" )) {
	if(argc>1)
	    specks_set_speed( st, getfloat(argv[1], clock_speed(st->clk)) );
	msg("speed %g steps per anim second", clock_speed(st->clk));

  } else if(!strcmp( argv[0], "timealign" )) {

	float datatime = 0, realtime = 0;
	/* Compute timebase so datastep <datatime> falls at <clocktime>,
	 * given current speed setting.
	 */
	double speed = clock_speed(st->clk) * st->clk->fwd;
	double timebase;
	if(getfloats( &datatime, 1, 1, argc,argv ) > 0 &&
		getfloats( &realtime, 1, 2, argc,argv ) > 0) {
	    specks_set_timebase( st, datatime - realtime*speed );
	    msg("Aligning datastep %g at realtime %g", datatime, realtime);
	} else if(argc>1) {
	    msg("Usage: timealign <datatime> <realtime>");
	}
	timebase = clock_timebase(st->clk);
	if(speed == 0)
	    msg("timebase %g (always at that datastep since speed=0)", timebase);
	else
	    msg("timebase %g (datastep at realtime 0), or datastep %g at realtime %g",
		timebase, datatime, (datatime - timebase) / speed);
  } else if(!strcmp( argv[0], "timebase" )) {
	if(argc>1)
	    specks_set_timebase( st, getfloat(argv[1], clock_timebase(st->clk)) );
	msg("timebase %g (datastep at time 0)", clock_timebase(st->clk));

  } else if(!strcmp( argv[0], "skipblanktimes" )) {
	if(argc>1)
	    st->skipblanktimes = getbool(argv[1], st->skipblanktimes);
	msg("blanktimes %s", st->skipblanktimes ? "on (skip blank timesteps)"
					: "off (show blankness at blank timesteps)");
	
  } else if(!strcmp( argv[0], "run" )) {
	parti_set_running( st, getbool( argv[1], 1 ) );
	st->playnext = 0.0;

  } else if(!strcmp( argv[0], "depthsort" )) {
	if(argc > 1) st->depthsort = getbool(argv[1], st->depthsort);
	msg("depthsort %s", st->depthsort ? "on" : "off" );

  } else if(!strcmp( argv[0], "fade" )) {
	char *fmt = "fade what?";
	if(argc>1) {
	    if(!strncmp(argv[1],"sph",3) || !strncmp(argv[1],"rad",3))
		st->fade = F_SPHERICAL;
	    else if(!strncmp(argv[1],"pla",3))
		st->fade = F_PLANAR;
	    else if(!strncmp(argv[1],"con",3) || !strncmp(argv[1],"ort",3))
		st->fade = F_CONSTANT;
	    else if(!strncmp(argv[1],"knee",3))
		st->fade = F_KNEE12;
	    else if(!strncmp(argv[1],"lin",3))
		st->fade = F_LINEAR;
	    else {
		msg("fade {sph|planar|const|linear|knees}");
		return -1;
	    }
	}
	if(argc>2) sscanf(argv[2], "%f", &st->fadeknee2);
	if(argc>3) sscanf(argv[3], "%f", &st->knee2steep);
	if(argc>4) sscanf(argv[4], "%f", &st->fadeknee1);
	if(argc>5) sscanf(argv[5], "%f%*c%f%*c%f",
			&st->fadecen.x[0], &st->fadecen.x[1], &st->fadecen.x[2]);
	if(argc>6) sscanf(argv[6], "%f", &st->fadecen.x[1]);
	if(argc>7) sscanf(argv[7], "%f", &st->fadecen.x[2]);
	switch(st->fade) {
	case F_SPHERICAL: fmt = "fade spherical  (1/r^2 from eyepoint)"; break;
	case F_PLANAR:    fmt = "fade planar     (1/r^2 from eye plane)"; break;
	case F_CONSTANT:  fmt = "fade const %g   (as if seen at given dist)"; break;
	case F_LINEAR:    fmt = "fade linear %g  (1/r, scaled to match planar at dist)"; break;
	case F_KNEE12:	  fmt = "fade knees %g %g  %g (fardist, steepness, neardist)"; break;
	case F_LREGION:	  fmt = "fade lregion %g  %g %g  %g %g %g (refdist; steepness, Rregion)"; break;
	}
	msg(fmt, st->fadeknee2, st->knee2steep, st->fadeknee1,
		st->fadecen.x[0], st->fadecen.x[1], st->fadecen.x[2]);

  } else if(!strcmp( argv[0], "clipbox" ) || !strcmp( argv[0], "cb" )) {
	Point cen, rad;
	int k;
	switch(argc) {
	case 2:	st->clipbox.level = getbool(argv[1], st->clipbox.level);
		if(!strcmp(argv[1], "hide")) st->clipbox.level = -1;
		break;
	case 3: if(3==sscanf(argv[1], "%f%*c%f%*c%f",
				&cen.x[0],&cen.x[1],&cen.x[2])
			&& 0 < (k = sscanf(argv[2], "%f%*c%f%*c%f",
				&rad.x[0],&rad.x[1],&rad.x[2]))) {
		    if(k==1) rad.x[1] = rad.x[2] = rad.x[0];
		    vsub( &st->clipbox.p0, &cen, &rad );
		    vadd( &st->clipbox.p1, &cen, &rad );
		    if(st->clipbox.level==0)
			st->clipbox.level = 1;	/* activate */
		} else {
		    msg("clipbox: xmin,xmax ymin,ymax zmin,zmax  or cenx,y,z radiusx,y,z");
		}
		break;
	case 4:
		if(2 == sscanf(argv[1], "%f%*c%f", &cen.x[0], &rad.x[0]) &&
		   2 == sscanf(argv[2], "%f%*c%f", &cen.x[1], &rad.x[1]) &&
		   2 == sscanf(argv[3], "%f%*c%f", &cen.x[2], &rad.x[2])) {
		    st->clipbox.p0 = cen;
		    st->clipbox.p1 = rad;
		    if(st->clipbox.level==0)
			st->clipbox.level = 1;	/* activate */
		} else {
		    msg("clipbox: xmin,xmax ymin,ymax zmin,zmax  or cenx,y,z radiusx,y,z");
		}
		break;

	case 7: for(k = 0; k < 3; k++) {
		    if(sscanf(argv[k+1], "%f", &st->clipbox.p0.x[k]) <= 0
			|| sscanf(argv[k+4], "%f", &st->clipbox.p1.x[k]) <= 0)
			break;
		}
		if(k == 3) {
		    if(st->clipbox.level == 0)
			st->clipbox.level = 1;
		} else {
		    msg("clipbox: xmin ymin zmin  xmax ymax zmax");
		}
		break;
	default:
		msg("clipbox: on|off|hide | x0,x1 y0,y1 z0,z1 | Cenx,y,z Rx,y,z | x0 y0 z0 x1 y1 z1");
		break;
	}
	vcomb( &cen, .5, &st->clipbox.p0, .5, &st->clipbox.p1 );
	vcomb( &rad, -.5, &st->clipbox.p0, .5, &st->clipbox.p1 );
	msg("clipbox %s  (%g,%g  %g,%g  %g,%g  or  %g,%g,%g %g,%g,%g)",
		st->clipbox.level ? (st->clipbox.level<0 ? "hide":"on") :"off",
		st->clipbox.p0.x[0], st->clipbox.p1.x[0],
		st->clipbox.p0.x[1], st->clipbox.p1.x[1],
		st->clipbox.p0.x[2], st->clipbox.p1.x[2],
		cen.x[0],cen.x[1],cen.x[2],  rad.x[0],rad.x[1],rad.x[2]);

  } else if(!strcmp( argv[0], "object" ) || sscanf( argv[0], "g%d", &i ) > 0) {
	struct stuff *tst = st;
	int a = argv[0][0]=='g' ? 0 : 1;
	char *ep;
	i = parti_object( argv[a], &tst, 0 );
	ep = argv[a] ? strchr(argv[a], '=') : NULL;

	if(a && argc==1) {
	    msg(tst->alias ? "object g%d=%s" : "object g%d", i, tst->alias);

	} else if(i<0) {
	    /* never mind */

	} else if(ep) {
	    parti_set_alias( tst, ep+1 );
	    msg("g%d = %s", i, ep+1);

	} else if(a+2 == argc && argv[a+1][0] == '=') {
	    parti_set_alias( tst, argv[a+2] );
	    msg("g%d = %s", i, argv[a+2]);

	} else if(a+1 < argc) {
	    specks_parse_args( &tst, argc-a-1, argv+a+1 );

	} else {
	    char *alias = parti_get_alias( tst );
	    st = tst;
	    msg(alias ? "object g%d=%s selected (%d particles)" :
			"object g%d%.0s selected (%d particles)",
		i, alias,
		specks_count( st->sl ));
	}

  } else if(!strcmp( argv[0], "gall" ) || !strncmp( argv[0], "allobj", 6 )) {
	int verbose = (argc>1 && !strcmp(argv[1], "-v"));
	parti_allobjs( argc-1-verbose, argv+1+verbose, verbose );

  } else if(!strcmp(argv[0], "tfm")) {
	int inv = 0;
	int mulWorldside = 0, mulObjside = 0;
	int hpr = 0;
	int calc = 0;
	int any, more, a0;
	float scl;
	Matrix ot, t;
	Point xyz;
	float aer[3];
	char *key;

	i = 1;
	key = argv[i];
	if(key == NULL) key = "";
	for(more = 1; *key != '\0' && more; key++) {
	  switch(*key) {
	  case '=': break;
	  case '*': mulWorldside = 1; break;
	  case '/': inv = 1; break;
	  case 'h': case 'p': case 'r': hpr = 1; break;
	  case ' ': case '\t': break;
	  case '\0':
		if(i < argc-1) {
		    key = argv[++i];
		    break;
		}
		/* else fall into default */
	  default: more = 0; key--; break;
	  }
	}

	parti_geto2w( st, parti_object( NULL, &st, 0 ), &ot );

	
	a0 = i;
	argv[a0] = key;
	for(any = 0; any < 16 && a0+any<argc
			&& sscanf(argv[a0+any], "%f", &t.m[any]) > 0; any++)
		;
	switch(any) {
	case 1:
		scl = t.m[0];
		t = Tidentity;
		t.m[0*4+0] = t.m[1*4+1] = t.m[2*4+2] = scl;
		break;
	case 6:
	case 7:		/* ignore fovy if included */
		xyz = *(Point *)&t.m[0];
		if(hpr) {
		    aer[0] = t.m[3], aer[1] = t.m[4], aer[2] = t.m[5];
		} else {
		    /* Note we permute: px py pz rx ry rz == px py pz e a r */
		    aer[1] = t.m[3], aer[0] = t.m[4], aer[2] = t.m[5];
		}
		xyzaer2tfm( &t, &xyz, aer );
		break;

	case 16: break;
	case 0:  t = ot; break;
	default:
	    msg("Usage: tfm [*] [/] [\"hpr\"] [16 numbers or 6 numbers] [=]");
	    return -1;
	}

	for(i = a0+any-1; i < argc; i++) {
	    if(strchr(argv[i], '=')) calc = 1;
	    if(strchr(argv[i], '*')) mulObjside = 1;
	}

	if(any) {
	    if(inv)
		eucinv( &t, &t );
	    if(mulWorldside)
		mmmul( &t, &ot, &t );
	    if(mulObjside)
		mmmul( &t, &t, &ot );

	    /* with trailing '=', just print result without assignment */

	    if(!calc)
		parti_seto2w( st, parti_object( NULL, &st, 0 ), &t );
	}

	scl = tfm2xyzaer( &xyz, aer, &t );
	msg("obj2w:  x y z %s   %g %g %g  %g %g %g  %g",
		hpr?"h p r":"rx ry rz",
		xyz.x[0],xyz.x[1],xyz.x[2],  aer[1-hpr],aer[hpr],aer[2], scl );
	for(i = 0; i < 3; i++)
	    msg(" %10.7f %10.7f %10.7f %10.7f", t.m[i*4+0],t.m[i*4+1],t.m[i*4+2],t.m[i*4+3]);
	msg(" %10.7g %10.7g %10.7g %10.7g", t.m[3*4+0],t.m[3*4+1],t.m[3*4+2],t.m[3*4+3]);
	eucinv( &t, &t );
	scl = tfm2xyzaer( &xyz, aer, &t );
	msg("w2obj:  x y z %s   %g %g %g  %g %g %g  %g",
		hpr?"h p r":"rx ry rz",
		xyz.x[0],xyz.x[1],xyz.x[2],  aer[1-hpr],aer[hpr],aer[2], scl );
	for(i = 0; i < 3; i++)
	    msg(" %10.7f %10.7f %10.7f %10.7f", t.m[i*4+0],t.m[i*4+1],t.m[i*4+2],t.m[i*4+3]);
	msg(" %10.7g %10.7g %10.7g %10.7g", t.m[3*4+0],t.m[3*4+1],t.m[3*4+2],t.m[3*4+3]);

  } else if(!strcmp( argv[0], "bgcolor" )) {
	char *result, given[64];
	if(argc == 4) {
	    sprintf(given, "%.10s,%.10s,%.10s", argv[1],argv[2],argv[3]);
	    result = parti_bgcolor( given );
	} else {
	    result = parti_bgcolor( argv[1] );
	}
	msg("bgcolor %s", result);

#if !CAVEMENU
  } else if(!strcmp( argv[0], "move" ) || !strcmp( argv[0], "move-objects" )) {
	i = parti_move( argv[1], &st );
	msg(i>=0 ? "move-objects on ; g%d selected" : "move-objects off", i);
#endif

#if MATT_VIRDIR
  } else if(!strcmp( argv[0], "time" )) {
	float newnow, now;
	if(argc > 1 && sscanf(argv[1], "%f", &newnow) > 0)
	    now = vd_camera_time( &newnow );
	else 
	    now = vd_camera_time( NULL );
	msg("time %g", now);

  } else if(!strcmp( argv[0], "key" )) {
	int curkey;
	if(argc == 1) {
	    curkey = vd_create_key();
	} else {
	    curkey = vd_select_key( atoi(argv[1]), 0 );
	}
	msg("key %d", curkey);

  } else if(!strcmp( argv[0], "attach" )) {
	vd_attach( getbool( argv[1], 1 ) );

  } else if(!strcmp( argv[0], "rem" )) {
	vd_remove_key( argc, argv );
  
#endif /* MATT_VIRDIR */

  } else if(!strcmp( argv[0], "step" )) {
	if(argc>1) {
	    if(argv[1][0] == '+') {
		if(argv[1][1] != '\0')
		    clock_set_step( st->clk, getfloat(&argv[1][1], 0) );
		clock_step( st->clk, 1 );
	    } else if(!strcmp(argv[1], "-")) {
		clock_step( st->clk, -1 );
	    } else {
		clock_set_time( st->clk, getfloat(argv[1], 0) );
	    }
	}


#if !CAVEMENU
	parti_set_running( st, 0 );
	specks_set_timestep(st);
#endif
	msg("step %g", st->currealtime);

  } else if(!strcmp( argv[0], "trange" ) || !strcmp( argv[0], "steprange" )) {
	double utmin = st->utmin;
	double utmax = st->utmax;
	double utwrap = st->utwrap;
	if(argc>1) {
	    if(!strcmp(argv[1],"on")) st->usertrange = 1;
	    else if(!strcmp(argv[1],"off")) st->usertrange = 0;
	    else {
		st->utmin = getfloat( argv[1], utmin );
		if(argc>2) st->utmax = getfloat( argv[2], utmax );
		if(argc>3) st->utwrap = getfloat( argv[3], utwrap );
		if(utmin != st->utmin || utmax != st->utmax)
		    st->usertrange = 1;
	    }
	}
	msg(st->usertrange ?
	    "trange %lg %lg  %lg (tmin tmax twrap)"
	  : "trange off   (%lg %lg  %lg  tmin tmax twrap)",
		st->utmin, st->utmax, st->utwrap);

  } else if(!strcmp( argv[0], "fwd" )) {
	if(argc>1)
	    clock_set_fwd( st->clk, getbool(argv[1], 1) );
	IFMENU( set_fwd( clock_fwd(st->clk), ment.fwd, st ) );

  } else if(!strcmp( argv[0], "gscale" )) {
	if(argc>1) sscanf(argv[1], "%f", &st->gscale);
	msg("gscale %g (scaling particles), then translating by %g %g %g",
		st->gscale, st->gtrans.x[0],st->gtrans.x[1],st->gtrans.x[2]);

  } else if(!strcmp( argv[0], "clearobj" )) {
	struct specklist **slp;
	struct mesh *m, *nm;
	struct ellipsoid *e, *ne;
	slp = specks_timespecksptr( st, st->curdata, st->curtime );
	specks_discard( st, slp );
	st->sl = *slp;
	for(m = st->staticmeshes, st->staticmeshes = NULL; m; m = nm) {
	    nm = m->next;
	    Free(m);
	}
	for(e = st->staticellipsoids, st->staticellipsoids = NULL; e; e = ne) {
	    ne = e->next;
	    Free(e);
	}

  } else if(!strcmp( argv[0], "every" )) {
	if(argc>1) sscanf(argv[1], "%d", &st->subsample);
	if(st->subsample <= 0) st->subsample = 1;
	msg("display every %dth particle (of %d)",
			st->subsample, specks_count(st->sl));

  } else if(!strncmp( argv[0], "everycomp", 9 )) {
	st->everycomp = getbool(argv[1], st->everycomp);
	msg("everycomp %d (%s compensate for \"every\" subsampling)",
		st->everycomp,
		st->everycomp ? "do" : "don't");

  } else if(!strncmp( argv[0], "color", 5 )) {
	struct valdesc *vd;
	if(argc>1) {
	    if(!specks_set_byvariable( st, argv[1], &st->coloredby ))
		st->coloredby = CONSTVAL;
	}
	vd = &st->vdesc[st->curdata][st->coloredby];
	if(argc>2 && (!strncmp(argv[2], "int", 3) ||
		      !strncmp(argv[2], "exact", 5))) {
	    vd->call = 0;
	    vd->cexact = 1;
	    vd->cmin = 0;
	    argc--, argv++;
	}
	if(argc>2 && (!strncmp(argv[2], "cont", 4) || !strcmp(argv[2], "-exact"))) {
	   vd->cexact = 0;
	   argc--, argv++;
	}
	if(argc>2 && sscanf(argv[2], "%f", &vd->cmin))
	    vd->call = 0;
	if(argc>3 && sscanf(argv[3], "%f", &vd->cmax))
	    vd->call = 0;
	if((vd->cmin == vd->cmax && vd->cmin == 0) ||
	   (argc>2 && (!strcmp(argv[2],"-") || !strcmp(argv[2],"all")))) {

	    vd->cmin = vd->min, vd->cmax = vd->max, vd->call = 1;
	}
	if(argc>4 && st->coloredby == CONSTVAL) sscanf(argv[4], "%f", &vd->mean);
	if(st->coloredby == CONSTVAL) {
	    msg("coloring-by rgb %.3f %.3f %.3f",
		vd->cmin, vd->cmax, vd->mean);
	} else if(vd->cexact) {
	    msg("coloring-by %d(%s) exactly (cindex=data+%g; data %g..%g, cmap 0..%d)",
		st->coloredby, vd->name, vd->cmin,
		vd->min,vd->max, st->ncmap-1);
	} else {
	    if(vd->min == vd->cmin && vd->max == vd->cmax || vd->nsamples==0) {
		msg("coloring-by %d(%s) %g.. %g mean %.3g]",
		    st->coloredby, vd->name,
		    vd->cmin, vd->cmax, vd->mean);
	    } else {
		msg("coloring-by %d(%s) %g.. %g (data %g..%g mean %.3g)",
		    st->coloredby, vd->name,
		    vd->cmin, vd->cmax, vd->min, vd->max, vd->mean);
	    }
	}
	if(argc>1)
	    st->colorseq++;

  } else if(!strcmp( argv[0], "datavar" ) || !strcmp( argv[0], "dv" )) {
	int min = 0, max = CONSTVAL-1;
	if(argc>1 && specks_set_byvariable( st, argv[1], &min ))
	    max = min;
	if(st->ndata>1)
	    msg("In dataset %d %s:", st->curdata, st->dataname[st->curdata]);
	for(i = min; i <= max; i++) {
	    struct valdesc *vd = &st->vdesc[st->curdata][i];
	    if(max>min && vd->name[0] == '\0' && vd->max == vd->min)
		continue;
	    msg("datavar %d %s  %g.. %g mean %g%s%s",
		i, vd->name[0]=='\0'?"\"\"" : vd->name,
		vd->min, vd->max, vd->mean,
		st->sizedby==i ? "[lum]" : "",
		st->coloredby==i ? "[color]" : "");
	}

  } else if(!strcmp( argv[0], "datawait" )) {
	if(argc>1) {
	    switch(i = getbool(argv[1],-1)) {
	    case 0:
	    case 1:
		st->datasync = i;
		break;
	    }
	    msg("datawait %s", st->datasync ? "on" : "off");
	}
	specks_datawait(st);

  } else if(!strncmp( argv[0], "lum", 3 )) {
	struct valdesc *vd;
	if(argc>1) {
	    if(!specks_set_byvariable( st, argv[1], &st->sizedby ))
		st->sizedby = CONSTVAL;
	}
	vd = &st->vdesc[st->curdata][st->sizedby];
	if(argc>2) vd->lall = 0, vd->lmin = getfloat(argv[2], vd->lmin);
	if(argc>3) vd->lall = 0, vd->lmax = getfloat(argv[3], vd->lmax);
	if(vd->lmin == vd->lmax && vd->lmin == 0 ||
	       argc>2 && (!strcmp(argv[2],"-") || !strcmp(argv[2],"all"))) {
	    vd->lmin = vd->min, vd->lmax = vd->max, vd->lall = 1;
	}
	if(st->sizedby == CONSTVAL) {
	    msg("lum-by constant %g", vd->lmin);
	} else {
	    msg("lum-by %d (%s) [%g..%g mean %.3g]", st->sizedby,
		vd->name, vd->lmin, vd->lmax, vd->mean);
	}
	if(argc>1)
	    st->sizeseq++;

  } else if(!strcmp( argv[0], "cmap" ) || !strcmp( argv[0], "boxcmap" )
					|| !strcmp( argv[0], "textcmap" )) {
	char *tfname = argv[1];
	char *realfile = findfile( NULL, argv[1] );
	if(argc != 2) {
	    msg("Usage: %s <ascii-file-of-RGB-triples>", argv[0]);
	} else {
	    if(argc > 1 && realfile == NULL) {
		tfname = (char *)alloca( strlen(argv[1]) + 6 );
		sprintf(tfname, "%s.cmap", argv[1]);
		realfile = findfile( NULL, tfname );
	    }
	    if(realfile != NULL) {
		if(argv[0][0] == 'b') {
		    specks_read_cmap( st, realfile, &st->boxncmap, &st->boxcmap );
		} else if(argv[0][0] == 't') {
		    specks_read_cmap( st, realfile, &st->textncmap, &st->textcmap );
		} else {
		    specks_read_cmap( st, realfile, &st->ncmap, &st->cmap );
		    st->colorseq++; /* Must recompute particle coloring */
		}
	    } else {
		msg("%s: can't find \"%s\" nor ....cmap", argv[0], argv[1]);
	    }
	}

  } else if(!strcmp( argv[0], "vcmap" )) {
	int k = 1;
	int coloredby = st->coloredby;
	char *tfname, *realfile;
	struct valdesc *vd;
	if(argc > 3 && !strcmp( argv[1], "-v" )) {
	    if(!specks_set_byvariable( st, argv[2], &coloredby )) {
		msg("vcmap -v %s: unknown colorby-variable", argv[2]);
		return -1;
	    }
	    k = 3;
	}
	vd = &st->vdesc[st->curdata][coloredby];
	if(coloredby < 0 || coloredby >= MAXVAL) {
	    msg("vcmap: bad colorby-variable %d", coloredby);
	    return -1;
	}
	if(argc != k+1) {
	    msg("Usage: vcmap [-v varname] <ascii-file-of-RGB-triples>", argv[0]);
	} else {
	    tfname = argv[k];
	    realfile = findfile( NULL, tfname );
	    if(realfile == NULL) {
		tfname = (char *)alloca( strlen(argv[k]) + 6 );
		sprintf(tfname, "%s.cmap", argv[k]);
		realfile = findfile( NULL, tfname );
	    }
	    if(realfile != NULL) {
		specks_read_cmap( st, realfile, &vd->vncmap, &vd->vcmap );
		st->colorseq++; /* Must recompute particle coloring */
	    } else {
		msg("vcmap: can't find \"%s\" nor ....cmap", argv[k]);
	    }
	}

  } else if(!strncmp( argv[0], "cment", 5 ) || !strncmp( argv[0], "boxcment", 8 )
		|| !strncmp( argv[0], "textcment", 6 )) {
	int i;
	unsigned char *cvp;
	char *prefix = "";
	int ncmap = st->ncmap;
	struct cment *cmap = st->cmap;

	switch(argv[0][0]) {
	case 'b': prefix = "box"; ncmap = st->boxncmap; cmap = st->boxcmap; break;
	case 't': prefix = "text"; ncmap = st->textncmap; cmap = st->textcmap; break;
	}

	if(argc>1 && argv[1][0] != '?') {
	    i = atoi(argv[1]);
	    if(i < 0 || i > ncmap) {
		msg("cment: %d out of range (must be 0..%d)", i, ncmap-1);
	    } else {
		if(argc>2) {
		    char *colorstr = rejoinargs( 2, argc, argv );
		    editcmap( st, ncmap, cmap, argv[1], argv[0], colorstr );
		    if(st->sl) st->colorseq++;	/* force recolor */
		}
		cvp = (unsigned char *)&cmap[i].raw;
		msg( "%scment %d  %.3f %.3f %.3f  (of cmap 0..%d",
			prefix,
			i, cvp[0]/255., cvp[1]/255., cvp[2]/255.,
			ncmap-1 );
	    }
	} else {
	    msg("Usage: %scment colorindex [r g b]  -or-  c1-c2,c3,c4  r g b -or- c1-c2,... = cindex", prefix);
	    msg(" (no blanks around commas for color ranges)");
	}

  } else if(!strncmp( argv[0], "only", 4 )) {
	int plus = !strcmp(argv[0], "only+");
	int minus = !strcmp(argv[0], "only-");
	int exact = !strcmp(argv[0], "only=");
	int tvar = -1;
	int all = 0;
	int k;
	int nspecks, nvisible, needthresh;
	struct specklist *sl;
	struct speck *sp;

	if(argc>1) {
	    if(!strcmp(argv[1], "all") || !strcmp(argv[1], "*")) all = 1;
	    if(!strcmp(argv[1], "none")) all = -1;
	}
	if(!all && ((!plus && !minus && !exact) ||
		!specks_set_byvariable( st, argv[1], &tvar))) {
	    msg("only+/only-/only=  \"all\"|\"none\"|varname value minvalue-maxvalue ...");
	}

	plus |= exact;

	st->selseq++;
	if(exact || all) {
	    /* preset all values */
	    int wipebit = (plus && all>0) ? 0 : THRESHBIT;
	    SelMask selbit = (plus && all>0) ? 0 : SELMASK(SEL_THRESH);
	    for(sl = st->sl; sl != NULL; sl = sl->next) {
		SelMask *sel = sl->sel;
		if((sp = sl->specks) == NULL || sl->text != NULL)
		    continue;
		for(k = sl->nspecks; --k >= 0; sp = NextSpeck(sp, sl, 1)) {
		    sp->rgba = (sp->rgba & ~THRESHBIT) | wipebit;
		    sel[k] = (sel[k] & ~SELMASK(SEL_THRESH)) | selbit;
		}
		sl->selseq = st->selseq;
	    }
	}

	/*
	 * If a thresh variable is given, use "plus" to choose
	 * whether to make that speck visible.
	 * NOTE that specks with THRESHBIT on are *invisible*!
	 */
	for(i = 2; i < argc; i++) {
	    float vmin = -1e38, vmax = 1e38;

	    if(argv[i][0] == '<') vmax = atof(argv[i]+1);
	    else if(argv[i][0] == '>') vmin = atof(argv[i]+1);
	    else {
		switch(sscanf(argv[i], "%f%*c%f", &vmin, &vmax)) {
		case 1: vmax = vmin; break;
		case 2: break;
		default:
		    msg("%s: \"%s\": expected N or <N or >N or N-M",
			argv[0], argv[i]);
		    return -1;
		}
	    }

	    for(sl = st->sl; sl != NULL; sl = sl->next) {
		SelMask *sel = sl->sel;
		if((sp = sl->specks) == NULL || sl->text != NULL || tvar<0
			|| SMALLSPECKSIZE(tvar+1) > sl->bytesperspeck)
		    continue;
		for(k = sl->nspecks; --k >= 0; sp = NextSpeck(sp, sl, 1)) {
		    if(sp->val[tvar]>=vmin && sp->val[tvar]<=vmax) {
			sp->rgba = plus ? sp->rgba & ~THRESHBIT
					: sp->rgba | THRESHBIT;
			sel[k] = plus ? sel[k] | SELMASK(SEL_THRESH)
			    	      : sel[k] & ~SELMASK(SEL_THRESH);
		    }
		}
		sl->selseq = st->selseq;
	    }
	}

	nspecks = nvisible = 0;
	for(sl = st->sl; sl != NULL; sl = sl->next) {
	    if((sp = sl->specks) == NULL || sl->text != NULL)
		continue;
	    nspecks += sl->nspecks;
	    for(k = sl->nspecks; --k >= 0; sp = NextSpeck(sp, sl, 1)) {
		if(!(sp->rgba & THRESHBIT))
		    nvisible++;
	    }
	}

	needthresh = (st->usethresh&P_USETHRESH)==0 && nvisible < nspecks;
	if(needthresh)
	    st->usethresh |= P_USETHRESH;
	msg("only %d visible of %d%s", nvisible, nspecks,
		needthresh ? " (selecting \"thresh on\")" : "");

  } else if(!strncmp( argv[0], "thresh", 6 )) {
	int i, expect = -1;
	float v;
	char smin[16], smax[16], *arg;
	char chosen[40];
	int changed = 0;
	char *selstr = NULL;
	for(i = 1; i < argc; i++) {
	    arg = argv[i];
	    if(!strcmp(arg,"-s")) {
		selstr = argv[++i];
	    } else if(!strcmp(arg,"off"))
		st->usethresh &= ~P_USETHRESH, expect = -1;
	    else if(!strcmp(arg,"on"))
		st->usethresh |= P_USETHRESH, expect = -1;
	    else if(expect<0 && specks_set_byvariable(st, arg, &st->threshvar)) {
		expect = 0;
		st->usethresh = P_USETHRESH;	/* neither MIN nor MAX set */
		changed = 1;
	    } else if(!strcmp(arg,"min") || !strcmp(arg, ">"))
		expect = 0;
	    else if(!strcmp(arg,"max") || !strcmp(arg, "<"))
		expect = 1;
	    else if(!strcmp(arg,"="))
		expect = 2;
	    else if(!strcmp(arg, "-")) {
		st->usethresh &= expect ? ~P_THRESHMAX : ~P_THRESHMIN;
		expect = 1;
	    } else if(sscanf(arg, "%f", &v) > 0) {
		if(expect != 1) {	/* -1, 0, or 2 */
		    st->thresh[0] = v;
		    st->usethresh |= P_THRESHMIN | P_USETHRESH;
		    changed = 1;
		}
		if(expect >= 1) {	/* 1 or 2 */
		    st->thresh[1] = v;
		    st->usethresh |= P_THRESHMAX | P_USETHRESH;
		    changed = 1;
		}
		expect = 1;
	    } else if(sscanf(arg, ">%f", &st->thresh[0]) > 0) {
		st->usethresh |= P_THRESHMIN | P_USETHRESH;
		changed = 1;
	    } else if(sscanf(arg, "<%f", &st->thresh[1]) > 0) {
		st->usethresh |= P_THRESHMAX | P_USETHRESH;
		changed = 1;
	    } else if(sscanf(arg, "=%f", &st->thresh[0]) > 0) {
		st->thresh[1] = st->thresh[0];
		st->usethresh |= P_THRESHMIN | P_THRESHMAX | P_USETHRESH; 
		changed = 1;
	    }
	}
	if(changed) {
	    st->threshseq++;
	    st->selseq++;
	    if(!parse_selexpr( st, selstr ? selstr : "thresh", &st->threshsel, NULL, NULL )) {
		msg("thresh -s %s: expected selection variable name", selstr);
		return 1;
	    }

	    /* "thresh" with no -s implies also "see thresh" */
	    if(selstr == NULL && st->seesel.wanted == 0)
		seldest2src( st, &st->threshsel, &st->seesel );
	    specks_reupdate( st, st->sl );
	}
	chosen[0] = '\0';
	sprintf(chosen, " (%s selected)", selcounts( st, st->sl, &st->threshsel ));
	sprintf(smin, st->usethresh&P_THRESHMIN ? "%g" : "-", st->thresh[0]);
	sprintf(smax, st->usethresh&P_THRESHMAX ? "%g" : "-", st->thresh[1]);

	msg(st->usethresh == 0 ? "thresh off"
		: "thresh%s%s%s %d(%s) min %s max %s%s",
		selstr ? " -s " : "",
		selstr ? selstr : "",
		st->usethresh&P_USETHRESH ? "" : " off ",
		st->threshvar, st->vdesc[st->curdata][st->threshvar].name,
		smin, smax, chosen);
	
  } else if(!strcmp( argv[0], "rawdump" )) {
	FILE *f = argc>1 ? fopen(argv[1], "w") : NULL;
	struct specklist *sl;
	int i, j, nmine, net;
	if(f == NULL) {
	    msg("%s: can't create", argv[1]);
	} else {
	    fprintf(f, "# from dataset %d \"%s\" timestep %d\n",
		st->curdata, CURDATATIME(fname), st->curtime);
	    for(i = 0; st->vdesc[st->curdata][i].name[0] != '\0'; i++) {
		fprintf(f, "%s\t", st->vdesc[st->curdata][i].name);
	    }
	    fprintf(f, "\n");
	    nmine = i;
	    net = 0;
	    sl = specks_timespecks( st, st->curdata, st->curtime );
	    if(sl == NULL && st->ntimes <= 1)
		sl = st->sl;
	    for(; sl != NULL; sl = sl->next) {
		for(j = 0; j < sl->nspecks; j++) {
		    for(i = 0; i < nmine; i++)
			fprintf(f, "%g\t", (NextSpeck(sl->specks, sl, j)->val[i]));
		    fprintf(f, "\n");
		    net++;
		}
	    }
	    fclose(f);
	    msg("Wrote %d particles (%d fields each) to %s",
		net, nmine, argv[1]);
	}
	
  } else if(!strncmp( argv[0], "slum", 3 ) || !strcmp( argv[0], "scale-lum" )) {
	float *lp;
	int by = st->sizedby, cd = st->curdata;
	if(argc>1) {
	    if(argc>2) specks_set_byvariable( st, argv[2], &by );
	    if(argc>3) sscanf(argv[3], "%d", &cd);
	    if((unsigned int)cd >= MAXFILES) cd = 0;
	    lp = &st->vdesc[cd][by].lum;
	    *lp = getfloat( argv[1], *lp!=0 ? *lp : 1 );
	}
	msg("slum scaling %g (var %d %s dataset %s (%d))",
	    st->vdesc[cd][by].lum, by, st->vdesc[cd][by].name,
	    st->dataname[cd], cd);

  } else if(!strcmp( argv[0], "seedata" )) {
	if(argc>1) {
	    int v = -1, len = strlen(argv[1]);
		/* search st->dataname[] array! XXX */
	    if(sscanf(argv[1], "%d", &v) > 0) {
		/* fine */
	    } else {
		for(v = st->ndata; --v >= 0; ) {
		    if(!strncasecmp( argv[1], st->dataname[v], len ))
			break;
		}
		if(v < 0) {
		    char msgstr[2048];
		    sprintf(msgstr,
			"Never heard of dataset \"%s\"; we have these %d:",
			argv[1], st->ndata);
		    for(v = 0; v < st->ndata; v++)
			sprintf(msgstr+strlen(msgstr), " \"%s\"(%d) ", st->dataname[v], v);
		    msg(msgstr);
		    v = -1;
		}
	    }
	    if(v >= 0 && v < st->ndata)
		st->curdata = v;
	}
	msg("showing dataset %d (%s)", st->curdata,
		st->dataname[st->curdata]);

  } else if(!strncmp( argv[0], "seesel", 3 )) {
    if(argc>1)
	parse_selexpr( st, rejoinargs( 1, argc, argv ), NULL, &st->seesel, "see" );
    msg("see %s (%s)", show_selexpr( st, NULL, &st->seesel ),
	    	selcounts( st, st->sl, &st->seesel ));

  } else if(!strcmp( argv[0], "emph" )) {
    if(argc>2 && !strcmp(argv[1], "-f"))
	st->emphfactor = getfloat( argv[2], st->useemph ),
        argc -= 2, argv += 2;
    if(argc>1) {
	if(!strcmp(argv[1], "on")) st->useemph = 1;
	else if(!strcmp(argv[1], "off")) st->useemph = 0;
	else {
	    parse_selexpr( st, rejoinargs( 1, argc, argv ), NULL, &st->emphsel, "emph" );
	    st->useemph = 1;
	}
    }
    if(st->useemph) {
	st->sizeseq++;
	msg("emph -f %g  %s (%s emphasized)", st->emphfactor,
	    	show_selexpr( st, NULL, &st->emphsel ),
		selcounts( st, st->sl, &st->emphsel ));
    } else {
	msg("emph off");
    }

  } else if(!strcmp( argv[0], "sel" )) {
    SelOp src, dest;
    char *rest = rejoinargs( 1, argc, argv );

    /*
     * sel a = b -c d  (existing names, or "see" or "emph" or maybe "thresh")
     * sel a = [all|none]
     */
    if(strchr(rest, '=')) {
	if(parse_selexpr( st, rest, &dest, &src, "sel" )) {
	    struct specklist *sl;
	    /* Is this the right place to look?  Do we want to update more than st->sl? */
	    int selcount = 0;
	    st->selseq++;
	    for(sl = st->sl; sl != NULL; sl = sl->next) {
		int k, ns = sl->nspecks;
		SelMask *sel = sl->sel;
		if(sl->text != NULL || sl->nsel < ns) continue;
		for(k = 0; k < ns; k++) {
		    if(src.use != SEL_NONE && SELECTED(sel[k], &src)) {
			SELSET(sel[k], &dest);
			selcount++;
		    }
		}
		if(st->useemph)
		    specks_resize( st, sl, sl->sizedby );  /* XXX could anything else depend on this? */
		sl->selseq = st->selseq;
	    }
	    msg("sel %s : %d of %d selected",
		show_selexpr(st, &dest, &src), selcount, specks_count(st->sl));
	}
    } else {
	/* No '=' -- just asking how many match some pattern */
	if(parse_selexpr( st, rest, NULL, &src, "sel")) {
	    msg("sel %s : %s matched", show_selexpr( st, NULL, &src ),
		    	selcounts(st, st->sl, &src));
	}
    }

  } else if(!strcmp( argv[0], "pick" )) {
    /* pick [-a] [note|center|sel <name>] */
    /* kira pick track */
      msg("pick not yet implemented XXX");

  } else if(!strcmp( argv[0], "show" ) || !strcmp( argv[0], "hide" )
	  || !strncasecmp( argv[0], "showbox", 7 )
	  || !strncasecmp( argv[0], "hidebox", 7 )) {
	int doshow = argv[0][0] == 's';
	int i, level, mask = 0;
	if(doshow) st->useboxes = 1;
	for(i = 1; i < argc; i++) {
	    level = getbool( argv[i], -2 );
	    if(level == -1)
		mask = ~0;
	    else if(level >= 0)
		mask |= (1 << level);
	}
	if(doshow) st->boxlevelmask |= mask;
	else st->boxlevelmask &= ~mask;
	/* st->boxlevelmask &= (1 << st->boxlevels) - 1;
	 *  Don't do this -- we want "show all" to apply to levels that
	 *  we haven't read in yet.
	 */
#if CAVEMENU
	for(i = 0; i < st->boxlevels && i < COUNT(ment.boxlevel); i++) {
	    struct boxleveler blr;
	    blr.st = st;
	    blr.level = i;
	    if(ment.boxlevel[i])
		set_boxlevel( 0, ment.boxlevel[i], &blr );
	}
#endif

  } else if(!strcmp( argv[0], "box" ) || !strcmp( argv[0], "boxes" )) {
	st->useboxes = getbool( argv[1], (st->useboxes+1)%3 );
	msg("boxes %s", st->useboxes==2 ? "ONLY"
				: st->useboxes ? "ON" : "off");

  } else if(!strncmp( argv[0], "boxlabel", 6 )) {
	if(argc>1) st->boxlabels = getbool( argv[1], !st->boxlabels );
	else st->boxlabels = !st->boxlabels;
	msg("boxlabels %s", st->boxlabels?"on":"off");

  } else if(!strncmp( argv[0], "boxaxes", 5 )) {
	if(argc>1) st->boxaxes = getbool( argv[1], !st->boxaxes );
	else st->boxaxes = !st->boxaxes;
	msg("boxaxes %s", st->boxaxes?"on":"off");

  } else if(!strcmp(argv[0], "boxscale")) {
	char buf[512];
	int from, to;
	if(argc>1) {
	    float v = atof(argv[1]);
	    from = getbool(argc>2?argv[2]:NULL, 0);
	    to = getbool(argc>3?argv[3]:NULL, MAXBOXLEV-1);
	    if(from<0) from = 0;
	    if(to>MAXBOXLEV-1) to=MAXBOXLEV-1;
	    while(from<=to)
		st->boxscale[from++] = v;
	}
	sprintf(buf, "boxscales:");
	for(to=MAXBOXLEV-1; to>1 && st->boxscale[to]==st->boxscale[to-1]; to--)
	    ;
	for(from = 0; from <= to; from++)
	    sprintf(buf+strlen(buf), " %g", st->boxscale[from]);
	msg("%s", buf);

  } else if(!strcmp( argv[0], "go" ) || !strcmp( argv[0], "gobox" )) {
	int boxno = getbool( argv[1], -1 );
	if(boxno < 0) {
	    msg("Usage: gobox <boxnumber> -- sets POI and jumps to view that AMR box");
	} else {
	    specks_gobox( st, boxno, argc-2, argv+2 );
	}

  } else if(!strcmp( argv[0], "goboxscale" )) {
	if(argc>1) sscanf(argv[1], "%f", &st->goboxscale);
	msg("goboxscale %g  (\"gobox\" sets scale to %g * box diagonal; 0 => leave scale intact)",
		st->goboxscale, st->goboxscale);

  } else if(!strcmp( argv[0], "psize" ) || !strcmp(argv[0], "pointsize")) {
	if(argc>1) {
	    st->psize = getfloat( argv[1], st->psize );
	    IFMENU( set_psize( st->psize, ment.psize, st ) );
	}
	msg("pointsize %g pixels (times scale-lum value)", st->psize );

  } else if(!strcmp( argv[0], "polysize" )) {
	if(argc>1) {
	    st->polysize = getfloat(argv[1], st->polysize);
	    IFMENU( set_polysize( st->polysize, ment.polysize, st ) );
	}
	if(argc>2) {
	    if(argv[2][0] == 'a' || argv[2][0] == 's') st->polyarea = 1;
	    else if(argv[2][0] == 'r') st->polyarea = 0;
	}
	msg("polysize %g%s", st->polysize, st->polyarea ? " area":"" );

  } else if(!strncmp( argv[0], "polylum", 7 )) {
	char *area = "";
	if(argc>1) {
	    if(!specks_set_byvariable( st, argv[1], &st->polysizevar ))
		if(!strcmp(argv[1], "-1") ||
		   !strcmp(argv[1], "point-size") ||
		   !strcmp(argv[1], "pointsize"))
		    st->polysizevar = -1;
	    if(st->polysizevar == CONSTVAL)
		st->polysizevar = -1;	/* Can't do const */
	}
	if(argc>2)
	    sscanf(argv[2], "%f", &st->polysize);
	switch(argv[argc-1][0]) {
	    case 'r':
	    case 'd':
	    case 's': st->polyarea = 0; break;
	    case 'a': st->polyarea = 1; break;
	}
	if(st->polyarea) area = " area";
	if(st->polysizevar < 0)
	    msg("polylumvar point-size%s", area);
	else
	    msg("polylumvar %d(%s)%s", st->polysizevar,
		st->vdesc[st->curdata][st->polysizevar].name,
		area);

  } else if(!strncmp( argv[0], "polyminpixels", 7 )) {
	if(argc>1) st->polymin = getfloat(argv[1], st->polymin);
	if(argc>2) st->polymax = getfloat(argv[2], st->polymax);
	msg("polyminpixels %g %g (minpixels maxpixels)", st->polymin, st->polymax );

  } else if(!strncmp( argv[0], "labelminpixels", 7 )) {
	st->textmin = getfloat(argv[1], st->textmin);
	msg("labelminpixels %g", st->textmin );


  } else if(!strcmp( argv[0], "labelsize" ) || !strcmp(argv[0], "lsize")) {
	st->textsize = getfloat(argv[1], st->textsize);
	msg("labelsize %g", st->textsize);

  } else if(!strcmp( argv[0], "point" ) || !strcmp( argv[0], "points" )) {
	st->usepoint = getbool( argv[1], !st->usepoint );
	IFMENU( set_point( st->usepoint, ment.point, st ) );
	msg("points %s", st->usepoint ? "on":"off");

  } else if(!strcmp( argv[0], "poly" ) || !strncmp( argv[0], "polygon", 7 )) {
	st->usepoly = getbool( argv[1], !st->usepoly );
	IFMENU( set_poly( st->usepoly, ment.poly, st ) );
	msg("polygons %s", st->usepoly ? "on":"off");

  } else if(!strncmp( argv[0], "texture", 7 ) || !strcmp( argv[0], "tx" )) {
	if(argc > 1 && !strcmp(argv[1], "preload")) {
	    msg("Preloading textures...");
	    for(i = 0; i < st->ntextures; i++)
		if(st->textures[i]) txload( st->textures[i] );
	} else if(argc > 1 && !strncmp(argv[1], "report", 3)) {
	    if(argc == 2) {
		for(i = 0; i < st->ntextures; i++)
			if(st->textures[i]) st->textures[i]->report = ~0;
	    } else {
		for(i = 2; i < argc; i++) {
		    int t = atoi(argv[i]);
		    if(t >= 0 && t < st->ntextures && st->textures[t])
			st->textures[t]->report = ~0;
		}
	    }
	} else {
	    st->usetextures = getbool( argv[1], !st->usetextures );
	    msg("textures %s", st->usetextures ? "on":"off");
	}

  } else if(!strcmp( argv[0], "txscale" )) {
	if(argc>1) sscanf(argv[1], "%f", &st->txscale);
	msg("txscale %.3f", st->txscale);

  } else if(!strncmp( argv[0], "polyorivar", 10)) {
	if(argc>1) st->polyorivar0 = getbool(argv[1], -1);
	msg(st->polyorivar0 >= 0
	  ? "polyorivar %d : polygon orientations from var %d..%d"
	  : "polyorivar %d : polygon orientations parallel to screen",
	  st->polyorivar0, st->polyorivar0, st->polyorivar0+5);

  } else if(!strcmp( argv[0], "texturevar")) {
	if(argc>1) st->texturevar = getbool(argv[1], -1);
	msg("texturevar %d", st->texturevar);

  } else if(!strncmp( argv[0], "ellipsoids", 5 )) {
	st->useellipsoids = getbool(argv[1], !st->useellipsoids);
	msg("ellipsoids %s", st->useellipsoids ? "on":"off");
	
  } else if(!strncmp( argv[0], "meshes", 4 )) {
	st->usemeshes = getbool(argv[1], !st->usemeshes);
	msg("meshes %s", st->usemeshes ? "on":"off");

  } else if(!strncmp( argv[0], "mullions", 3 )) {
      	st->mullions = getfloat( argv[1], st->mullions );
	msg("mullions %g", st->mullions);

  } else if(!strcmp( argv[0], "label" ) || !strcmp( argv[0], "labels" )) {
	st->usetext = getbool( argv[1], !st->usetext );
	IFMENU( set_label( st->usetext, ment.label, st ) );
	msg("labels %s", st->usetext ? "on":"off");

  } else if(!strcmp( argv[0], "laxes" )) {
	st->usetextaxes = getbool(argv[1], !st->usetextaxes);
	msg("laxes %s  (axes on each label)", st->usetextaxes ? "on":"off");

  } else if(!strncmp( argv[0], "polyside", 8 )) {
	if(argc>1) sscanf(argv[1], "%d", &st->npolygon);
	msg("polysides %d (polygons drawn with %d sides)",
		st->npolygon,st->npolygon);

  } else if(!strcmp( argv[0], "gamma" )) {
	if(argc>1) sscanf(argv[1], "%f", &st->gamma);
	msg("gamma %g", st->gamma);

  } else if(!strncmp( argv[0], "cgamma", 4 ) || !strncmp(argv[0], "setgamma", 6)) {
	float *p = NULL;
	int any = 0;
	for(i = 1; i < argc; i++) {
	    if(!strcmp(argv[i], "-b")) i++, p = &st->rgbright[0];
	    else if(!strcmp(argv[i], "-g")) i++, p = &st->rgbgamma[0];
	    else if(i == argc-1) p = &st->rgbgamma[0];
	    else continue;
	    switch(sscanf(argv[i], "%f%*c%f%*c%f", &p[0],&p[1],&p[2])) {
	    case 1: p[1] = p[2] = p[0];  /* and fall into... */
	    case 3: any = 1; break;
	    default: msg("setgamma [-b BRIGHT] [-g GAMMA]: want 1 or 3 floats not %s", argv[i]);
	    }
	}
	msg("setgamma -b %g,%g,%g  -g %g,%g,%g",
		st->rgbright[0],st->rgbright[1],st->rgbright[2],
		st->rgbgamma[0],st->rgbgamma[1],st->rgbgamma[2]);
	if(any)
	    specks_rgbremap( st );

  } else if(!strcmp( argv[0], "alpha" )) {
	if(argc>1)
	    st->alpha = getfloat(argv[1], st->alpha);
	IFMENU( set_alpha( st->alpha, ment.alpha, st ) );
	msg("alpha %g", st->alpha);

  } else if(!strncmp( argv[0], "fast", 4 )) {
	st->fast = getbool(argv[1], !st->fast);
	if(argc > 2) sscanf(argv[2], "%f", &st->pfaint);
	if(argc > 3) sscanf(argv[3], "%f", &st->plarge);
	msg("fast %s  (ptsize %.3g %.3g (faintest largest))",
		st->fast ? "on" : "off (better rendering)",
		st->pfaint, st->plarge);

  } else if(!strncmp( argv[0], "ptsize", 6 )) {
	if(argc > 1) sscanf(argv[1], "%f", &st->pfaint);
	if(argc > 2) sscanf(argv[2], "%f", &st->plarge);
	msg("ptsize  %.3g %.3g  (faintest largest)", st->pfaint, st->plarge);

  } else if(!strcmp( argv[0], "fog" ) && argc > 1) {
	IFMENU( set_fog( st->fog, ment.fog, st ) );

  } else if(!strcmp( argv[0], "menu" ) || !strcmp( argv[0], "fmenu" )) {
#if CAVEMENU
	if(argc == 4 && !strncmp(argv[1], "pos", 3)) {
	    menu_setpos( pmenu, atof(argv[2]), atof(argv[3]) );

	} else if(argc == 3 && !strcmp(argv[1], "wall")) {
	    menu_onwall( pmenu, atoi(argv[2]) );
	    menu_onwall( stubmenu, atoi(argv[2]) );

	} else if(argc >= 2 && !strncmp(argv[1], "hid", 3)) {
	    st->hidemenu = argc>2 ? getbool( argv[2], st->hidemenu ) :
			 st->hidemenu ? 0 : -1;
	    msg("fmenu hide %s", st->hidemenu ? "on" : "off");

	} else if(argc >= 2 && !strcmp(argv[1], "show")) {
	    st->hidemenu = argc>2 ? getbool( argv[2], st->hidemenu ) :
			st->hidemenu ? 0 : -1;
	    msg("fmenu show %s", st->hidemenu ? "off" : "on");

	} else if(argc >= 2 && !strncmp(argv[1], "h", 1)) {
	    if(argc>2) menu_setheight( pmenu, atof(argv[2]) );
	    msg("fmenu height %g", pmenu->yspan);

	} else if(argc >= 2 && !strncmp(argv[1], "demandfps", 6)) {
	    if(argc == 3) sscanf(argv[2], "%f", &st->menudemandfps);
	    msg("fmenu demandfps %g", st->menudemandfps);

	} else if(argc > 1 && argc <= 3 && !strncmp(argv[1], "font", 4)) {
	    if(argc > 2) {
		menu_setfont( pmenu, argv[2] );
		menu_setfont( stubmenu, argv[2] );
	    }
	    msg("fmenu font %s", menu_getfont(pmenu));

	} else if(argc == 2 && atof(argv[1]) != 0) {
	    menu_setheight( pmenu, atof(argv[1]) );

	} else if(argc == 3) {
	    menu_setpos( pmenu, atof(argv[1]), atof(argv[2]) );

	} else if(argc <= 1 || argv[1][0] == '?' || !strncmp(argv[1],"help",4)) {
	    msg("\"fmenu ?\" for help");
	    msg("fmenu pos %.3f %.3f (xpos ypos, in pixels)", pmenu->x0, pmenu->y0);
	    msg("fmenu height %.3f (fraction of screen height; negative = inverted)", pmenu->yspan);
	    msg("fmenu wall %d (cave.h wall number)", pmenu->cavewall);
	    msg("fmenu hide [on|off]");
	    msg("fmenu font %s (X font name)", menu_getfont(pmenu));
	    msg("fmenu demandfps %g (hide scene if FPS too low)", st->menudemandfps);
	} else {
	    msg("fmenu: expected \"pos\" or \"height\" or \"hide\" or \"font\" or \"wall\".");
	    msg("Try just \"fmenu\" for help.");
	}
#endif

  } else if(!strcmp( argv[0], "datascale" ) && argc>1) {
	float s, v = atof(argv[1]);
	if(v!=0) {
	    struct specklist *sl;
	    struct speck *p;
	    int i;
	    for(sl = st->sl; sl != NULL; sl = sl->next) {
		s = v / sl->scaledby;
		for(i = 0, p = sl->specks; i < sl->nspecks; i++, p = NextSpeck(p, sl, 1)) {
		    p->p.x[0] *= s; p->p.x[1] *= s; p->p.x[2] *= s;
		}
		sl->scaledby = v;
	    }
	}
  } else if(!strcmp( argv[0], "where" ) || !strcmp( argv[0], "w" )) {
	tellwhere(st);

  } else if(!strcmp( argv[0], "version" )) {
	extern char partiview_version[];
	char *cp = strchr(local_id, ',');	/* ... partibrains.c,v N.NN */
	if(cp) {
	    cp += 3;
	    msg("version %s (brains %.*s)", partiview_version, strcspn(cp, " "), cp);
	}
  }
  else
    return 0;

  *stp = st;	/* in case it changed, e.g. because of an "object" cmd */
  return 1;
}