CCL Home Page
Up Directory CCL ScianSpaces
/*ScianSpaces.c
  Space objects
  Eric Pepke
  April 6, 1990
*/

#include "Scian.h"
#include "ScianTypes.h"
#include "ScianColors.h"
#include "ScianLists.h"
#include "ScianArrays.h"
#include "ScianLights.h"
#include "ScianSpaces.h"
#include "ScianEvents.h"
#include "ScianWindows.h"
#include "ScianDraw.h"
#include "ScianObjWindows.h"
#include "ScianVisWindows.h"
#include "ScianVisObjects.h"
#include "ScianDialogs.h"
#include "ScianScripts.h"
#include "ScianIcons.h"
#include "ScianIDs.h"
#include "ScianControls.h"
#include "ScianButtons.h"
#include "ScianTextBoxes.h"
#include "ScianTitleBoxes.h"
#include "ScianFontSystem.h"
#include "ScianPerspec.h"
#include "ScianErrors.h"
#include "ScianTimers.h"
#include "ScianSliders.h"
#include "ScianStyle.h"
#include "ScianComplexControls.h"
#include "ScianMethods.h"
#include "ScianPick.h"
#include "ScianPreferences.h"
#include "ScianFiles.h"
#include "ScianObjFunctions.h"
#include "ScianTemplates.h"
#include "ScianTemplateHelper.h"
#include "ScianSymbols.h"
#include "ScianDepend.h"
#include "ScianDatasets.h"
#include "ScianFilters.h"
#include "ScianDatabase.h"
#include "ScianAnimation.h"
#include "ScianPictures.h"
#include "ScianSnap.h"

Bool phsco = false;			/*True iff phscologram is on*/
Bool drawingTransparent = false;	/*True iff drawing only transparent*/
int clockDisplaySerialNum = 0;		/*Clock display serial number*/
extern real fps;
ObjPtr spacePickedObject = NULLOBJ;

int qualityDrawnSoFar;		/*Max quality drawn so far*/
int minDrawingQuality;		/*Minimum quality to draw*/
int maxDrawingQuality;		/*Maximum quality to draw*/

#define PLAYCLOCK (0.1)			/*Rate for clock play*/
#define FASTCLOCK (0.2)			/*Rate for clock fast*/

#define ROTLIGHTS			/*Rotate lights with object*/

#define DSPCLOCKWIDTH	97
#define DSPCLOCKHEIGHT	48
#define DSPCLOCKLEFT	55
#define DSPCLOCKTOP	70
#define DSPCLOCKTLEFT	10
#define DSPCLOCKTRIGHT	10
#define DSPCLOCKTTOP	23

/*Dial factors*/
#define DIALROTFACTOR	0.02	/*Factor for rotating with dials*/
#define DIALMOVEFACTOR	0.01	/*Factor for moving with dials*/
#define DIALANGLEFACTOR	0.001	/*Factor for changing angles*/
#define DIALWIDTHFACTOR 0.005	/*Factor for changing width*/
#define DIALBINOCFACTOR	0.01	/*Factor for changing binocularity*/

/*Stereo and blanking values for CrystalEyes*/
#define YSTEREO		491
#define YBLANK		41

#define ASPECT		(((float) (right - left)) / ((float) (top - bottom))) * (phsco ? 0.61 : 1.0)
#define MINROT		3		/*Minimum number of pixels to move to rotate*/

ObjPtr curSpace = 0;			/*Current space*/
real curFocusDist = 0.0;		/*Current focus distance*/
real curViewField = 0.0;		/*Current field of view.  Angle if 
					  perspec, width if ortho*/
real curViewClip[2];			/*Current viewing clipping distances*/
ObjPtr curSpaceLights;			/*Lights in the current space*/
int curWhichView;			/*Current which view*/
ObjPtr spaceClass = 0;			/*Class of a 3-D space*/
real spaceTime = 0.0;			/*Time for current space.  Cute name, huh?*/
ObjPtr spacePanelClass;			/*Invisible panel over a space*/
ObjPtr spaceBackPanelClass;		/*Colored panel behind a space*/
ObjPtr controllerClass;			/*Class for space controllers (clocks, lights, etc.)*/
ObjPtr clockClass;			/*Class of clocks*/
ObjPtr worldClass;			/*Class of object with world*/
ObjPtr placementClass;			/*Class of object with placement*/
ObjPtr stereoClass;			/*Class of object with stereo*/
ObjPtr observerClass;			/*Class of observers*/
ObjPtr rendererClass;			/*Class of renderers*/
Matrix viewerCoordsMatrix;		/*Matrix in viewer coordinates*/
Bool oneObserver = false;
Bool oneRenderer = false;
Bool oneClock = false;
Bool oneLights = false;			/*True iff to make only one controller*/
ObjPtr placementTrackClass;		/*Track for observer placement*/
ObjPtr jointClass;			/*Class of joints*/
real observerPosition[3];
VertexPtr bestPickVertex;		/*The best vertex from the last pick*/

void DrawSpaceContents(ObjPtr, int, int, int, int, int);

#ifdef HAVE_PROTOTYPES
static double f(double x)
#else
static double f(x)
double x;
#endif
/*Performs the increasing monotonic function required for the virtual trackball
  of click radius ratio x.  Yeah, right.*/
{
    if (x <= 0.0)
    {
	return 0.0;
    }
    else if (x >= 1.0)
    {
	return M_PI_2;
    }
    else
    {
	return M_PI_2 * x;
    }
}

Bool GetListExtent(list, bounds)
ObjPtr list;
real bounds[];
/*Puts the extent of the objects (or the objects they represent) in list 
  into bounds.  Returns true iff it's nonzero*/
{
    ThingListPtr runner;

    /*Make bounds ludicrous*/
    bounds[0] = bounds[2] = bounds[4] = plusInf;
    bounds[1] = bounds[3] = bounds[5] = minusInf;

    runner = LISTOF(list);
    if (!runner)
    {
	return false;
    }
    while (runner)
    {
	ObjPtr repObj;
	ObjPtr var;

	repObj = runner -> thing;
	if (IsObject(repObj))
	{
	    MakeVar(repObj, BOUNDS);
	    var = GetVar(repObj, BOUNDS);
	    if (var)
	    {
		real localBounds[6];
		Array2CArray(localBounds, var);
		
		var = GetVar(repObj, XSCALE);
		if (var)
		{
		    real v;
		    v = GetReal(var);
		    localBounds[0] *= v;
		    localBounds[1] *= v;
		}
		var = GetVar(repObj, YSCALE);
		if (var)
		{
		    real v;
		    v = GetReal(var);
		    localBounds[2] *= v;
		    localBounds[3] *= v;
		}
		var = GetVar(repObj, ZSCALE);
		if (var)
		{
		    real v;
		    v = GetReal(var);
		    localBounds[4] *= v;
		    localBounds[5] *= v;
		}
		if (localBounds[0] < bounds[0]) bounds[0] = localBounds[0];
		if (localBounds[1] > bounds[1]) bounds[1] = localBounds[1];
		if (localBounds[2] < bounds[2]) bounds[2] = localBounds[2];
		if (localBounds[3] > bounds[3]) bounds[3] = localBounds[3];
		if (localBounds[4] < bounds[4]) bounds[4] = localBounds[4];
		if (localBounds[5] > bounds[5]) bounds[5] = localBounds[5];
	    }
	}
	runner = runner -> next;
    }

    /*If bounds ludicrous, make them sane*/
    if (bounds[0] == plusInf || bounds[2] == plusInf || bounds[4] == plusInf ||
	bounds[1] == minusInf || bounds[3] == minusInf || bounds[5] == minusInf)
    {
	bounds[0] = bounds[2] = bounds[4] = 0.0;
	bounds[1] = bounds[3] = bounds[5] = 1.0;
    }

    return true;
}

#define NUDGESIZE	2000		/*Size of one nudge*/
#define MAXNNUDGES	4		/*Maximum number of nudges*/
long zMin = 0 + NUDGESIZE * MAXNNUDGES;
long zMax = 0x7fffff - NUDGESIZE * MAXNNUDGES;
long curZMin, curZMax;

void NudgeCloser()
/*Nudges objects to be drawn closer*/
{
#ifdef GRAPHICS
    curZMax -= NUDGESIZE;
    curZMin -= NUDGESIZE;
    lsetdepth(curZMin, curZMax);
#endif
}

void NudgeFarther()
/*Nudges objects to be drawn farther*/
{
#ifdef GRAPHICS
    curZMax += NUDGESIZE;
    curZMin += NUDGESIZE;
    lsetdepth(curZMin, curZMax);
#endif
}

#ifdef HAVE_PROTOTYPES
void upat(float vx, float vy, float vz, 
          float px, float py, float pz, 
          float ux, float uy, float uz)
#else
void upat(vx, vy, vz, px, py, pz, ux, uy, uz)
float vx; float vy; float vz; 
float px; float py; float pz; 
float ux; float uy; float uz;
#endif
/*Replacement for lookat.  Specifies a viewer at (vx, vy, vz) looking at
  a point (px, py, pz) with an up vector of (ux, uy, uz).  Adapted from 
  Thant Tessman's code in 4Dgifts.*/
{
#ifdef GRAPHICS
    int i;
    float forward[3], side[3], up[3];
    float m[4][4];

    forward[0] = px - vx;
    forward[1] = py - vy;
    forward[2] = pz - vz;

    /* temporarily use view-up to hold world-up */
    up[0] = ux;	    
    up[1] = uy;
    up[2] = uz;

    NORMALIZE(forward);

    /* generate the vector side from the
     * 2 vectors view forward and world up 
     */
    CROSS(forward, up, side);
    NORMALIZE(side);

    /* generate the view up vector from 
     * the 2 vectors view forward and view side 
     */
    CROSS(side, forward, up);

    m[0][0] = side[0];
    m[1][0] = side[1];
    m[2][0] = side[2];
    m[3][0] = 0.0;

    m[0][1] = up[0];
    m[1][1] = up[1];
    m[2][1] = up[2];
    m[3][1] = 0.0;

    m[0][2] = -forward[0];
    m[1][2] = -forward[1];
    m[2][2] = -forward[2];
    m[3][2] = 0.0;

    m[0][3] = 0.0;
    m[1][3] = 0.0;
    m[2][3] = 0.0;
    m[3][3] = 1.0;
    multmatrix(m);
    translate(-vx, -vy, -vz);
#endif
}

#ifdef HAVE_PROTOTYPES
void StartSpace(ObjPtr space, int left, int right, int bottom, int top,
	int action, int whichView)
#else
void StartSpace(space, left, right, bottom, top, action, whichView)
ObjPtr space;
int left, right, bottom, top;
int action;
int whichView;
#endif
/*Start a drawing, rotation, or press in a space.  Action is the action
  that is being performed*/
{
#ifdef GRAPHICS
    real eyePosn[3];		/*Coords of the eye*/
    real focusPoint[3];		/*Coords of the focus point*/
    real upVector[3];		/*The up vector of the observer*/
    real forwardVector[3];
    ObjPtr object;
    ObjPtr psStuff;
    ObjPtr boundsArray;
    real bounds[4];
    real bigBounds[6];		/*Bounds of the objects*/
    Coord center[3];		/*The center of the data set*/
    ObjPtr contents;
    real scaleFactor[3];	/*Factor to scale*/
    real maxSize;		/*Maximum size of the objects this draw*/
    ObjPtr observer;		/*Observer of the space*/
    real eyeOffset;
    ObjPtr var;
    Matrix proj, model;
    float aspect;		/*Aspect ratio*/


    curWhichView = whichView;

    if (windowSystem == WS_GLWINDOWS)
    {
	/*Change bounds and aspect ratio depending on whichView*/
	aspect = ASPECT;
	switch(VIEWOF(whichView))
	{
	    case VIEW_XLEFT:
	    case VIEW_WRIGHT:
		left = (left + right) / 2 + 1;
		aspect = ASPECT;
		break;
	    case VIEW_XRIGHT:
	    case VIEW_WLEFT:
		right = (left + right) / 2;
		aspect = ASPECT;
		break;
	    case VIEW_CLEFT:
		bottom = YSTEREO + YBLANK;
		top = bottom + YSTEREO;
		break;
	    case VIEW_CRIGHT:
		bottom = 0;
		top = bottom + YSTEREO;
		break;
	}
    
	/*Set a new viewport to the current area*/
	SetSubPort(left, right, bottom, top);
    }

    observer = GetObjectVar("StartSpace", space, OBSERVER);
    if (!observer) return;

    if (windowSystem == WS_GLWINDOWS)
    {
	if (ISLEFT(whichView))
	{
		var = GetVar(observer, BINOCULARITY);
		eyeOffset = -GetReal(var) * 0.5;
	}
	else if (ISRIGHT(whichView))
	{
		var = GetVar(observer, BINOCULARITY);
		eyeOffset = GetReal(var) * 0.5;
	}
	else
	{
		eyeOffset = 0.0;
	}
    }

    /*Set up current viewing parameters*/
    var = GetVar(observer, FOCUSDIST);
    if (var)
    {
	curFocusDist = GetReal(var);
    }
    else
    {
	curFocusDist = INITEYEDIST;
    }

    var = GetVar(observer, VIEWFIELD);
    if (var)
    {
	curViewField = GetReal(var);
    }
    else
    {
	curViewField = INITAOV;
    }

    var = GetVar(observer, VIEWCLIP);
    if (var)
    {
	Array2CArray(curViewClip, var);
    }
    else
    {
	curViewClip[0] = INITNEARCLIP;
	curViewClip[1] = INITFARCLIP;
    }
    if (curViewClip[0] < MINCLIP) curViewClip[0] = MINCLIP;
    if (curViewClip[1] < MINCLIP) curViewClip[1] = MINCLIP;

    /*Get information about the eye*/
    GetObserverLocation(eyePosn, observer);
    GetFocusPoint(focusPoint, observer);
    GetAdjustedVectors(forwardVector, upVector, observer);

    if (windowSystem == WS_GLWINDOWS)
    {
	if (ISSTEREO(whichView))
	{
	    real sideVector[3];
	    real temp;
    
	    /*Have to slide eyePosn and focusPoint over somewhat*/
	    CROSS(forwardVector, upVector, sideVector);
    
	    /*Move eye over*/
	    eyePosn[0] += sideVector[0] * eyeOffset;
	    eyePosn[1] += sideVector[1] * eyeOffset;
	    eyePosn[2] += sideVector[2] * eyeOffset;
	}
    }

    /*Get information about the clock*/
    MakeVar(space, TIME);
    var = GetVar(space, TIME);
    if (var)
    {
	spaceTime = GetReal(var);
    }
    else
    {
	spaceTime = 0.0;
    }

    /*Set the current space*/
    curSpace = space;

    if (action == PICKSPACE)
    {
	StartPick();
    }

    if (windowSystem == WS_GLWINDOWS)
    {
#ifdef GL4D
	mmode(MPROJECTION);
#endif
    
	if (whichView & VIEW_MOD_ORTHO)
	{
	    /*Plain old orthographic projection.*/
	    ortho(-aspect * curViewField, aspect * curViewField,
		  -curViewField, curViewField, curViewClip[0], curViewClip[1]);
	}
	else
	{
	    /*Exciting perspective projection!!!*/
	    perspective((long) (curViewField * 10.0), aspect,
			curViewClip[0], curViewClip[1]);
	}
    
#ifdef GL4D
	mmode(MVIEWING);
	loadmatrix(Identity);
#endif
    }

    mmode(MVIEWING);
    upat(eyePosn[0], eyePosn[1], eyePosn[2],
	focusPoint[0], focusPoint[1], focusPoint[2], 
	upVector[0], upVector[1], upVector[2]);

    getmatrix(viewerCoordsMatrix);

    /*Set up the lights*/
    curSpaceLights = GetVar(space, LIGHTS);
    if (action == DRAWSPACE)
    {
	if (curSpaceLights)
	{
	    StartLights(space, curSpaceLights, false);
	}
    }

    if (action == DRAWSPACE)
    {
	int i;
	real radius;

	if (windowSystem == WS_GLWINDOWS)
	{
	    /*Shift filter for 3-D glasses*/
	    if (rgbp && (VIEWOF(whichView) == VIEW_CRLEFT || VIEWOF(whichView) == VIEW_RCRIGHT))
	    {
		BeginMask(false, true, true, true);
	    }
	    else if (rgbp && (VIEWOF(whichView) == VIEW_CRRIGHT || VIEWOF(whichView) == VIEW_RCLEFT))
	    {
		BeginMask(true, false, false, true);
	    }
	    else if (rgbp && (VIEWOF(whichView) == VIEW_GRLEFT || VIEWOF(whichView) == VIEW_RGRIGHT))
	    {
		BeginMask(false, true, false, true);
	    }
	    else if (rgbp && (VIEWOF(whichView) == VIEW_GRRIGHT || VIEWOF(whichView) == VIEW_RGLEFT))
	    {
		BeginMask(true, false, false, true);
	    }
	    else
	    {
		BeginMask(true, true, true, true);
	    }
    
	    if (VIEWOF(whichView) == VIEW_EOLEFT || VIEWOF(whichView) == VIEW_OERIGHT)
	    {
		BeginEvenOnly();
	    }
	    if (VIEWOF(whichView) == VIEW_OELEFT || VIEWOF(whichView) == VIEW_EORIGHT)
	    {
		BeginOddOnly();
	    }
	}

	/*Go to Gouraud shading*/
	shademodel(GOURAUD);

	if (windowSystem == WS_GLWINDOWS)
	{
	    /*Set up the Z-buffer*/
	    if (action == DRAWSPACE)
	    {
		curZMin = zMin + NUDGESIZE * MAXNNUDGES;
		curZMax = zMax - NUDGESIZE * MAXNNUDGES;
    
		lsetdepth(curZMin, curZMax);
		zbuffer(TRUE);
		zclear();
	    }
	}

	/*Draw rotation and motion guides*/
	if (whichView & VIEW_MOD_ORTHO)
	{
	    radius = SPACESPHERESIZE * 1.85 * curViewField;
	}
	else
	{
	    radius = SPACESPHERESIZE * curFocusDist * rsin(curViewField * M_PI / 180.0);
	}

	if (GetPredicate(curSpace, SHOWROTGUIDES))
	{
	    SetUIColor(UIWHITE);

	    DrawWFSphere(focusPoint[0], focusPoint[1], focusPoint[2], radius);
	    DrawSpaceLine(focusPoint[0] - radius, focusPoint[1], focusPoint[2],
			  focusPoint[0] + radius, focusPoint[1], focusPoint[2]);
	    DrawSpaceLine(focusPoint[0], focusPoint[1] - radius, focusPoint[2],
			  focusPoint[0], focusPoint[1] + radius, focusPoint[2]);
	    DrawSpaceLine(focusPoint[0], focusPoint[1], focusPoint[2] - radius,
			  focusPoint[0], focusPoint[1], focusPoint[2] + radius);
	}
	if (GetPredicate(curSpace, SHOWMOTGUIDES))
	{
	    real x, y;
	    SetUIColor(UIGRAY50);
	    for (x = -10.0; x <= 10.0; x += 1.0)
	    {
		for (y = -10.0; y <= 10.0; y += 1.0)
		{
		    DrawSpaceLine(x, y, -10.0, x, y, 10.0);
		    DrawSpaceLine(x, -10.0, y, x, 10.0, y);
		    DrawSpaceLine(-10.0, x, y, 10.0, x, y);
		}
	    }
	}
    }

    {
	if (GetPredicate(observer, AUTOADJUST))
	{
	    float normFactor;

	    /*Determine the size of the objects within*/ 
	    contents = GetVar(space, CONTENTS);
	    GetListExtent(contents, bigBounds);

	    /*Get their center*/
	    center[0] = (bigBounds[1] + bigBounds[0]) / 2.0;
	    center[1] = (bigBounds[3] + bigBounds[2]) / 2.0;
	    center[2] = (bigBounds[5] + bigBounds[4]) / 2.0;

	    /*Scale to a reasonable scaling factor for the data*/
	    maxSize = ABS(bigBounds[1] - bigBounds[0]);
	    if (ABS(bigBounds[3] - bigBounds[2]) > maxSize)
	    {
		maxSize = ABS(bigBounds[3] - bigBounds[2]);
	    }
	    if (ABS(bigBounds[5] - bigBounds[4]) > maxSize)
	    {
		maxSize = ABS(bigBounds[5] - bigBounds[4]);
	    }

	    switch (windowSystem)
	    {
		case WS_GLWINDOWS:
		    normFactor = 1.6 / maxSize;
		    break;
		case WS_CAVESIM:
		    normFactor = 5.0 / maxSize;
		    break;
	    } 
	    scaleFactor[0] = normFactor;
	    scaleFactor[1] = normFactor;
	    scaleFactor[2] = normFactor;
	    var = NewRealArray(1, 3L);
	    ((real *) ELEMENTS(var))[0] = 1.0 / scaleFactor[0];
	    ((real *) ELEMENTS(var))[1] = 1.0 / scaleFactor[1];
	    ((real *) ELEMENTS(var))[2] = 1.0 / scaleFactor[2];
	    SetVar(observer, SPACESCALE, var);

	    var = NewRealArray(1, 3L);
	    CArray2Array(var, center);
	    SetVar(observer, SPACEORIGIN, var);
	}
	else
	{
	    var = GetFixedArrayVar("BeginSpace", observer, SPACESCALE, 1, 3L);
	    if (var)
	    {
		Array2CArray(scaleFactor, var);
		scaleFactor[0] = 1.0 / scaleFactor[0];
		scaleFactor[1] = 1.0 / scaleFactor[1];
		scaleFactor[2] = 1.0 / scaleFactor[2];
	    }
	    else
	    {
		scaleFactor[0] = 1.0;
		scaleFactor[1] = 1.0;
		scaleFactor[2] = 1.0;
	    }

	    var = GetFixedArrayVar("BeginSpace", observer, SPACEORIGIN, 1, 3L);
	    if (var)
	    {
		Array2CArray(center, var);
	    }
	    else
	    {
		center[0] = 0.0;
		center[1] = 0.0;
		center[2] = 0.0;
	    }
	}

	/*Save observer position*/
    	observerPosition[0] = eyePosn[0] * scaleFactor[0] + center[0];
    	observerPosition[1] = eyePosn[1] * scaleFactor[1] + center[1];
    	observerPosition[2] = eyePosn[2] * scaleFactor[2] + center[2];

	scale(scaleFactor[0], scaleFactor[1], scaleFactor[2]);
	translate(-center[0], -center[1], -center[2]);
    }
#endif
}

#ifdef HAVE_PROTOTYPES
void StopSpace(int action)
#else
void StopSpace(action)
int action;
#endif
/*Stops drawing, rotating, or pressing in a space*/
{
#ifdef GRAPHICS
    if (action == DRAWSPACE)
    {
	if (windowSystem == WS_GLWINDOWS)
	{
	    real radius;
    
	    OptimizeSharpness();
    
	    if (curWhichView & VIEW_MOD_ORTHO)
	    {
		radius = SPACESPHERESIZE * 1.85 * curViewField;
	    }
	    else
	    {
		radius = SPACESPHERESIZE * curFocusDist * rsin(curViewField * M_PI / 180.0);
	    }
    
	    loadmatrix(viewerCoordsMatrix);
	    /*Draw the observer*/
	    {
		ObjPtr observer;
		FuncTyp method;
    
		observer = GetVar(curSpace, OBSERVER);
		if (observer)
		{
		    method = GetMethod(observer, DRAW);
		    if (method)
		    {
			(*method)(observer);
		    }
		}
	    }
	    /*Draw the lights*/
	    if (curSpaceLights)
	    {
		DrawLights(curSpace, curSpaceLights, action, radius);
		StopLights(curSpace);
	    }
	    EndMask();
	    if (VIEWOF(curWhichView) == VIEW_EOLEFT || VIEWOF(curWhichView) == VIEW_OERIGHT)
	    {
		EndEvenOnly();
	    }
	    if (VIEWOF(curWhichView) == VIEW_OELEFT || VIEWOF(curWhichView) == VIEW_EORIGHT)
	    {
		EndOddOnly();
	    }
	}
    }

    if (action == PICKSPACE)
    {
	spacePickedObject = StopPick(&bestPickVertex);
    }
    if (windowSystem == WS_GLWINDOWS)
    {

	if (action == DRAWSPACE)
	{
	    zbuffer(FALSE);
	}
	RestoreSubPort();

    }

#endif
    curSpace = NULLOBJ;
}

ObjPtr DrawObserver(observer)
ObjPtr observer;
/*Draws an observer*/
{
    ObjPtr keyList, result;

    if (drawingQuality < DQ_FULL) return ObjTrue;

    keyList = NewList();
    PostfixList(keyList, NewSymbol(CLASSID));
    PostfixList(keyList, NewInt(CLASS_SEQUENCE));

    zbuffer(FALSE);
    SetUIFont(UIBOLDNORMALFONT);
    SetUIColor(UIWHITE);

    result = SearchDatabase(keyList);
    if (result && LISTOF(result))
    {
	ThingListPtr runner;
	char *name;
	
	for (runner = LISTOF(result); runner; runner = runner -> next)
	{
	    ObjPtr track, snapshot;
	    real time;
	    real frameWidth;
	    int whichFrame;
	    ObjPtr var;
	    track = GetTrackedVarGroupWithinSequence(runner -> thing, observer, "Placement");
	    if (track)
	    {
		ObjPtr var;
		ObjPtr snap;
		ObjPtr *joints;
		ObjPtr *keyframes;
		int k;
		int nJoints;
		int nKeyframes;
		real loc[3];
		real vw[3];
		real l1[3], l2[3];
		int botFrame, topFrame;
		Quaternion qrot;	/*Quaternion for rotation*/

		var = GetVar(track, KEYFRAMES);
		if (!var) continue;
		nKeyframes = DIMS(var)[0];
		keyframes = ELEMENTS(var);

		for (k = 0; k < nKeyframes; ++k)
		{
		    char str[20];

		    snap = GetVar(keyframes[k], SNAPSHOT);

		    var = GetVar(snap, LOCATION);
		    Array2CArray(loc, var);

		    sprintf(str, "%d", k);
		    DrawSpaceString(loc[0], loc[1], loc[2],
			str);

		    var = GetVar(keyframes[k], WORLDVELOCITY);
		    if (var)
		    {
			Array2CArray(vw, var);

			DrawSpaceLine(loc[0], loc[1], loc[2],
			    loc[0] + vw[0] * 5.0,
			    loc[1] + vw[1] * 5.0,
			    loc[2] + vw[2] * 5.0);
		    }
		}

		/*Get the joints*/
		MakeVar(track, JOINTS);
		var = GetVar(track, JOINTS);
		if (!var) continue;
		nJoints = DIMS(var)[0];
		joints = ELEMENTS(var);

		/*Find the range of keyframes*/
		var = GetIntVar("DrawObserver", keyframes[0], WHICHFRAME);
		if (!var) return ObjTrue;
		botFrame = GetInt(var);

		var = GetIntVar("DrawObserver", keyframes[nKeyframes - 1], WHICHFRAME);
		if (!var) return ObjTrue;
		topFrame = GetInt(var);

		if (topFrame > botFrame)
		{
		    TweenObserverPlacement(track, botFrame, qrot, l1);
		    for (k = 0; k < topFrame; ++k)
		    {
			TweenObserverPlacement(track, k + 1, qrot, l2);

			SetUIColor(k & 1 ? UIWHITE : UICYAN);
			DrawSpaceLine(l1[0], l1[1], l1[2], l2[0], l2[1], l2[2]);

			l1[0] = l2[0];
			l1[1] = l2[1];
			l1[2] = l2[2];
		    }
		}
 	    }
	}
    }
    return ObjTrue;
}

static ObjPtr ForAllSpaceObjects(object, routine)
ObjPtr object;
FuncTyp routine;
/*Does routine on a space and its contents*/
{
    ObjPtr contents;
    ObjPtr lights;

    (*routine)(object);

    /*Now check my CONTENTS*/
    contents = GetVar(object, CONTENTS);
    if (contents)
    {
	ForAllObjects(contents, routine);
    }

    /*Now check the lights*/
    lights = GetVar(object, LIGHTS);
    if (lights)
    {
	ForAllObjects(lights, routine);
    }

    /*DIK also observer and renderer and clock*/

    return ObjTrue;
}


ObjPtr WakeObserver(observer, lateness)
ObjPtr observer;
double lateness;
/*Wakes an observer and makes it continue rotation*/
{
    ObjPtr var;
    Bool wakeAgain = false;	/*True iff wake again*/
    int x, y;
    real phi;

    DoNotDisturb(observer, MARKTIME);

    /*See if we need to spin*/
    var = GetVar(observer, ROTSPEED);
    if (var && 0.0 != (phi = GetReal(var)))
    {
	real axis[3];

	/*Turn phi from speed to amount*/
	phi *= lateness;

	var = GetFixedArrayVar("WakeObserver", observer, ROTAXIS, 1, 3L);
	if (var)
	{
	    Array2CArray(axis, var);

	    RotateObserver(observer, axis, phi, false);
	}

	wakeAgain = true;
    }
    /*See if we want to fly*/
    else if (GetPredicate(observer, FLYING))
    {
#ifdef INTERACTIVE
	ObjPtr var;
	real eyePosn[3];
	real roll, dPitch, pitch;
	float dTime;
	real perspecStuff[4];
	real airspeed;
	long mouseX, mouseY;
	real upVector[3];
	real forwardVector[3];
	real sp, cp, t;
	real temp[3];

	dTime = lateness;

	/*Move eye according to airspeed*/
	var = GetVar(observer, AIRSPEED);
	if (var && IsReal(var))
	{
	    airspeed = GetReal(var);
	}
	else
	{
	    airspeed = 0.0;
	}
	
	var = GetVar(observer, LOCATION);
	if (var && IsRealArray(var) && RANK(var) == 1 && 
	    DIMS(var)[0] == 3)
	{
	    Array2CArray(eyePosn, var);
	}
	else
	{
	    eyePosn[0] = 0.0;
	    eyePosn[1] = 0.0;
	    eyePosn[2] = INITEYEDIST;
	    var = NewRealArray(1, (long) 3);
	    SetVar(observer, LOCATION, var);
	}

	/*Get a new roll and dpitch value*/
	mouseX = getvaluator(MOUSEX);
	mouseY = getvaluator(MOUSEY);

	roll = (((real) mouseX) / (real) scrWidth) * 2 * MAXROLL - MAXROLL;
	dPitch = (((real) mouseY) / (real) scrHeight) * 2 * MAXDPITCH - MAXDPITCH;

	/*Update eye position based on airspeed*/

	/*Get pitch*/
	var = GetVar(observer, PITCH);
	if (var && IsReal(var))
	{
	    pitch = GetReal(var);
	}
	else
	{
	    pitch = 0.0;
	    var = NewReal(pitch);
	    SetVar(observer, PITCH, var);
	}
    
	/*Change the pitch based on dpitch, asymptotic to up and down*/
	if (dPitch > 0.0)
	{
	    pitch = pitch + (M_PI_2 - pitch) * dPitch * dTime;
	}
	else
	{
	    pitch = pitch + (pitch + M_PI_2) * dPitch * dTime;
	}
	SetVar(observer, PITCH, NewReal(pitch));

	/*Rotate around the up vector*/
	phi = roll * ROLLYAWFACTOR * dTime;
	GetUpVector(upVector, observer);
	RotateObserver(observer, upVector, phi, false);
	GetAdjustedVectors(forwardVector, upVector, observer);

	/*Move eye*/
	eyePosn[0] += forwardVector[0] * airspeed * dTime;
	eyePosn[1] += forwardVector[1] * airspeed * dTime;
	eyePosn[2] += forwardVector[2] * airspeed * dTime; 

	var = NewRealArray(1, 3L);
	CArray2Array(var, eyePosn);
	SetVar(observer, LOCATION, var);
    
	ImInvalid(observer);
	wakeAgain = true;
#endif
    }
    if (wakeAgain)
    {
	WakeMe(observer, MARKTIME, Clock() + 0.001);
    }
    return ObjTrue;
}

#ifdef HAVE_PROTOTYPES
void SetRotationMotor(real rotSpeed, real ax, real ay, real az)
#else
void SetRotationMotor(rotSpeed, ax, ay, az)
real rotSpeed, ax, ay, az;
#endif
/*Sets a rotation motor at rotSpeed around axis ax, ay, az*/
{
    ObjPtr var;
    real axis[3];
    char logLine[256];
    ObjPtr space, observer;

    if (!selWinInfo)
    {
	return;
    }

    space = FindSpace(selWinInfo);
    if (!space)
    {
	return;
    }

    observer = GetObjectVar("SetRotationMotor", space, OBSERVER);
    if (!observer)
    {
	return;
    }

    var = NewRealArray(1, 3L);
    axis[0] = ax;
    axis[1] = ay;
    axis[2] = az;
    CArray2Array(var, axis);

    SetVar(observer, ROTAXIS, var);
    SetVar(observer, ROTSPEED, NewReal(rotSpeed));

    if (logging)
    {
	sprintf(logLine, "set rotation %g [%g %g %g]\n",
		rotSpeed * 180.0 / M_PI, ax, ay, az);
	Log(logLine);
    }

    if (rotSpeed > 0.0)
    {
	DoNotDisturb(observer, MARKTIME);
	WakeMe(observer, MARKTIME, Clock() + 0.001);
    }
}

#ifdef HAVE_PROTOTYPES
void RecordObserver(ObjPtr observer)
#else
void RecordObserver(observer)
ObjPtr observer;
#endif
/*Records the current status of an observer*/
{
    ObjPtr keyList, result;

    keyList = NewList();
    PostfixList(keyList, NewSymbol(CLASSID));
    PostfixList(keyList, NewInt(CLASS_SEQUENCE));
    PostfixList(keyList, NewSymbol(RECORDING));
    PostfixList(keyList, ObjTrue);

    result = SearchDatabase(keyList);
    if (result && LISTOF(result))
    {
	ThingListPtr runner;
	char *name;

	runner = LISTOF(result);
	while (runner)
	{
	    ObjPtr track, snapshot;
	    real time;
	    real frameWidth;
	    int whichFrame;
	    ObjPtr var;
	    track = TrackVarGroupWithinSequence(runner -> thing, observer, "Placement", placementTrackClass);
	    if (track)
	    {
		ObjPtr list;

		MakeVar(runner -> thing, FRAMERATE);
		var = GetVar(runner -> thing, FRAMERATE);
		if (var)
		{
		    frameWidth = 1.0 / GetReal(var);
		}
		else
		{
		    frameWidth = 1.0 / 30.0;
		}

		var = GetVar(runner -> thing, TIME);
		if (var)
		{
		    time = GetReal(var);
		}
		else
		{
		    time = 0.0;
		}

		whichFrame = (int) floor(time / frameWidth + 0.5);
		
		list = NewList();
		PostfixList(list, NewSymbol(LOCATION));
		PostfixList(list, NewSymbol(ROTQUAT));
		PostfixList(list, NewSymbol(FOCUSDIST));
		snapshot = TakeVarsSnapshot(observer, list);
		InsertKeySnapshot(track, whichFrame, snapshot);
		SetVar(track, CHANGED, ObjTrue);
	    }
	    runner = runner -> next;
	}
    }
}

#ifdef HAVE_PROTOTYPES
void LogObserver(ObjPtr observer)
#else
void LogObserver(observer)
ObjPtr observer;
#endif
/*Logs an observer*/
{
    ObjPtr var;
    real temp[3];

    RecordObserver(observer);

    if (logging)
    {
	MakeObjectName(tempStr, observer);
	Log("begin snapshot ");
	Log(tempStr);
	Log("\n");
	LogVariable(observer, LOCATION);
	LogVariable(observer, ROTQUAT);
	LogVariable(observer, FOCUSDIST);
	Log("end snapshot\n");
    }
}

#ifdef HAVE_PROTOTYPES
ObjPtr MoveSpace(ObjPtr object, int x, int y, long flags)
#else
ObjPtr MoveSpace(object, x, y, flags)
ObjPtr object;
int x, y;
long flags;
#endif
/*Does a move in a space beginning at x and y.  Returns
  ObjTrue if it was a good move.*/
{
#ifdef INTERACTIVE
    int d;
    int newX, newY;
    ObjPtr objects, observer, corral;
    ThingListPtr runner;
    ObjPtr tempObj;
    real eyePosn[3];
    real up[3], forward[3], side[3];
    real moveVector[3];
    real uMove, vMove;
    real sr, cr;
    real sp, cp;
    real sy, cy;
    real sf;
    real focusDist;
    int left, right, bottom, top;

    Get2DIntBounds(object, &left, &right, &bottom, &top);

    d = top - bottom;

    corral = GetVar(object, CORRAL);

    SetRotationMotor(0.0, 0.0, 0.0, 1.0);

    SetVar(object, MOVING, ObjTrue);

    /*Get info about the observer*/
    observer = GetObjectVar("MoveSpace", object, OBSERVER);

    SaveForUndo(observer);

    GetObserverLocation(eyePosn, observer);
    GetAdjustedVectors(forward, up, observer);
    CROSS(forward, up, side);

    /*Get perspective stuff for scaling movement and eyeball distance*/
    tempObj = GetRealVar("MoveSpace", observer, FOCUSDIST);
    if (tempObj)
    {
	focusDist = GetReal(tempObj);
    }
    else
    {
	focusDist = INITEYEDIST;
    }

    if (flags & F_DOUBLECLICK)
    {
    
    }
    /*Press the contents of the space*/
    else while (Mouse(&newX, &newY))
    {
	if ((newX != x) || (newY != y))
	{
	    /*Get raw uMove and vMove*/
	    sf = focusDist * MOVEFACTOR;
	    uMove = sf * ((float) (x - newX)) / (float) d;
	    vMove = sf * ((float) (y - newY)) / (float) d;

	    moveVector[0] = uMove * side[0] + vMove * up[0];

	    /*Produce a motion vector*/
	    moveVector[0] = uMove * side[0] + vMove * up[0];
	    moveVector[1] = uMove * side[1] + vMove * up[1];
	    moveVector[2] = uMove * side[2] + vMove * up[2];

	    if (flags & F_SHIFTDOWN)
	    {
		int k, best = 0;
		float curDot, maxDot = 0.0;
		float rr;

		/*Constrain the motion axis to the nearest ortho axis*/
		for (k = 0; k < 3; ++k)
		{
		    curDot =
			moveVector[0] * Identity[k][0] +
			moveVector[1] * Identity[k][1] +
			moveVector[2] * Identity[k][2];

		    if (ABS(curDot) > ABS(maxDot))
		    {
			/*It's a better choice*/
			maxDot = curDot;
			best = k;
		    }
		}

		/*Now we have a best match*/
		moveVector[0] = maxDot * Identity[best][0];
		moveVector[1] = maxDot * Identity[best][1];
		moveVector[2] = maxDot * Identity[best][2];
	    }

	    eyePosn[0] += moveVector[0];
	    eyePosn[1] += moveVector[1];
	    eyePosn[2] += moveVector[2];

	    /*Put eyePosn back*/
	    tempObj = NewRealArray(1, 3L);
	    CArray2Array(tempObj, eyePosn);
	    SetVar(observer, LOCATION, tempObj);

	    x = newX;
	    y = newY;

	    ImInvalid(observer);
	    DrawMe(object);
	}
    }
    SetVar(object, MOVING, ObjFalse);

    if (logging)
    {
	LogObserver(observer);
    }
#endif
    return ObjTrue;
}

#ifdef HAVE_PROTOTYPES
ObjPtr RotateSpace(ObjPtr object, int x, int y, long flags)
#else
ObjPtr RotateSpace(object, x, y, flags)
ObjPtr object;
int x, y;
long flags;
#endif
/*Does a rotate in a space beginning at x and y.  Returns
  true iff the rotate really was in the panel.*/
{
    int left, right, bottom, top;
    ObjPtr observer/*, observers*/;
    ObjPtr lights;
    ThingListPtr runner;

#ifdef INTERACTIVE
    Get2DIntBounds(object, &left, &right, &bottom, &top);

    /*See if there are any lights to rotate*/
    lights = GetVar(object, LIGHTS);
    if (lights)
    {
	runner = LISTOF(lights);
	while (runner)
	{
	    if (IsSelected(runner -> thing))
	    {
		break;
	    }
	    runner = runner -> next;
	}
    }

    /*If none selected, say there are none*/
    if (!runner)
    {
	lights = NULLOBJ;
    }

    /*Get the observer*/
    observer = GetObjectVar("RotateSpace", object, OBSERVER);

    if (observer)
    {
	Bool motorOn = false;		/*True iff motor on*/
	float rotSpeed = 0.0;		/*Rotation speed*/
	float ax, ay, az;			/*Three components of axis of rotation*/
	real axis[3];			/*Final axis of rotation*/
	ObjPtr var;				/*Temporary var*/
	real eyePosn[3];	/*Observer info*/
	real focusDist;
	real focusPoint[3];
	real forward[3], up[3], side[3];	/*Observer axes*/

	var = GetRealVar("RotateSpace", observer, FOCUSDIST);
	if (var)
	{
	    focusDist = GetReal(var);
	}
	else
	{
	    focusDist = INITEYEDIST;
	}
	GetFocusPoint(focusPoint, observer);
	GetObserverLocation(eyePosn, observer);
	GetAdjustedVectors(forward, up, observer);

	CROSS(forward, up, side);
	NORMALIZE(side);

	if (flags & F_DOUBLECLICK)
	{
	    if (lights)
	    {
		/*Double click.  Snap lights to their closest values*/

		runner = LISTOF(lights);
		while (runner)
		{
		    if (IsSelected(runner -> thing))
		    {
			/*Rotate the light*/
			ObjPtr var;
			int i;
			float biggest;
			int ibiggest;
			real location[3];
	
			var = GetFixedArrayVar("RotateSpace", runner -> thing, LOCATION, 1, 3L);
			if (var)
			{
			    Array2CArray(location, var);
			    biggest = 0.0;
			    ibiggest = 0;
			    for (i = 0; i < 3; ++i)
			    {
				if (ABS(location[i]) > biggest)
				{
				    ibiggest = i;
				    biggest = ABS(location[i]);
				}
			    }
			    location[ibiggest] =
				location[ibiggest] > 0.0 ? 1.0 : -1.0;
			    for (i = 0; i < 3; ++i)
			    {
				if (i != ibiggest)
				{
				    location[i] = 0.0;
				}
			    }
			    var = NewRealArray(1, 3L);
			    CArray2Array(var, location);
			    SetVar(runner -> thing, LOCATION, var);
			    ImInvalid(runner -> thing);
			}
		    }
		    runner = runner -> next;
		}
	    }
	    else
	    {
		/*It was a double click.  Align forward and up to the
		  nearest orthogonal unit vector*/
		real newRot[3][3];	/*New rotation matrix*/
		Quaternion rotQuat;	/*New quaternion rotation matrix*/
		Bool set;		/*True iff this j has been set nonzero*/
		int i, j;		/*Counters within the matrix*/
		float biggest;	/*The biggest xform coefficient so far*/
		int jbiggest;	/*The j of biggest*/

		SaveForUndo(observer);
		/*First do forward*/
		biggest = 0.0;
		jbiggest = -1;
		for (j = 0; j < 3; ++j)
		{
		    if (ABS(forward[j]) > biggest)
		    {
			biggest = ABS(forward[j]);
			jbiggest = j;
		    }
		}
		if (jbiggest == -1)
		{
		    return ObjTrue;
		}
		for (j = 0; j < 3; ++j)
		{
		    if (j == jbiggest)
		    {
			forward[j] = forward[j] > 0 ? 1.0 : -1.0;
		    }
		    else
		    {
			forward[j] = 0.0;
		    }
		}

		/*Now do up*/
		biggest = 0.0;
		jbiggest = -1;
		for (j = 0; j < 3; ++j)
		{
		    if (ABS(up[j]) > biggest)
		    {
			biggest = ABS(up[j]);
			jbiggest = j;
		    }
		}
		if (jbiggest == -1)
		{
		    return ObjTrue;
		}
		for (j = 0; j < 3; ++j)
		{
		    if (j == jbiggest)
		    {
			up[j] = up[j] > 0 ? 1.0 : -1.0;
		    }
		    else
		    {
			up[j] = 0.0;
		    }
		}

		/*Make sure up is not the same as forward*/
		if ((ABS(forward[0]) == ABS(up[0])) &&
		    (ABS(forward[1]) == ABS(up[1])) &&
		    (ABS(forward[2]) == ABS(up[2])))
		{
		    /*Make a different up based on forward*/
		    up[0] = forward[1];
		    up[1] = forward[2];
		    up[2] = forward[0];
		}

		/*Make new variables for observer*/
		eyePosn[0] = focusPoint[0] - focusDist * forward[0];
		eyePosn[1] = focusPoint[1] - focusDist * forward[1];
		eyePosn[2] = focusPoint[2] - focusDist * forward[2];
		var = NewRealArray(1, 3L);
		CArray2Array(var, eyePosn);
		SetVar(observer, LOCATION, var);

		CROSS(forward, up, side);

		/*Make a matrix*/
		newRot[0][0] = side[0];
		newRot[1][0] = side[1];
		newRot[2][0] = side[2];

		newRot[0][1] = up[0];
		newRot[1][1] = up[1];
		newRot[2][1] = up[2];

		newRot[0][2] = -forward[0];
		newRot[1][2] = -forward[1];
		newRot[2][2] = -forward[2];

		MatrixToQuaternion(newRot, rotQuat);
		var = NewRealArray(1, 4L);
		CArray2Array(var, rotQuat);
		SetVar(observer, ROTQUAT, var);

		ImInvalid(observer);
		if (logging)
		{
		    LogObserver(observer);
		}
	    }
	    ImInvalid(object);
	}
	else
	{
	    /*It's a click and drag.  Do the trackball stuff.*/
	    real Ox, Oy;		/*Center of virtual trackball*/
	    real r;			/*Radius of virtual trackball*/
	    real omega;			/*Ratio of the click radius to the trackball radius*/
	    real theta;			/*Angle to the click radius*/
	    real tao;			/*Angle from the click radius to the direction*/
	    real phi;			/*Amount the trackball is pushed*/
	    int newX, newY;		/*New X and Y values*/
	    Bool firstRun = true;	/*The first run*/
	    real lastTime = 0.0;	/*The last time*/
	    real cumPhi = 0.0;		/*Cumulative phi*/
	    real curTime = 0.0;		/*The current time*/
	    int viewType;		/*The view type*/
	    int stereoMode;		/*The stereo mode*/

	    if (!lights)
	    {
		SaveForUndo(observer);
	    }

	    /*Get the view type*/
	    var = GetVar(observer, VIEWTYPE);
	    if (var)
	    {
		viewType = GetInt(var);
	    }
	    else
	    {
		viewType == VT_PERSPECTIVE;
	    }

	    var = GetVar(observer, STEREOMODE);
	    if (var)
	    {
		stereoMode = GetInt(var);
	    }
	    else
	    {
		stereoMode == SM_MONO;
	    }

	    SetVar(object, ROTATING, ObjTrue);

	    /*Calculate origin and radius of trackball*/
	    if (stereoMode == SM_CROSSEYED || viewType == SM_WALLEYED)
	    {
		if (x < (left + right) / 2)
		{
		    Ox = (left + right) / 4;
		}
		else
		{
		    Ox = 3 * (left + right) / 4;
		}
		Oy = (top + bottom) / 2;
	    }
	    else if (stereoMode == SM_LEFTONTOP)
	    {
		Ox = (left + right) / 2;
		if (y < YSTEREO)
		{
		    Oy = YSTEREO / 2;
		}
		else
		{
		    Oy = YSTEREO * 3 / 2 + YBLANK;
		}
	    }
	    else if (stereoMode == SM_RIGHTONTOP)
	    {
		Ox = (left + right) / 2;
		if (y < YSTEREO)
		{
		    Oy = YSTEREO / 2;
		}
		else
		{
		    Oy = YSTEREO * 3 / 2 + YBLANK;
		}
	    }
	    else
	    {
		Ox = (left + right) / 2;
		Oy = (top + bottom) / 2;
	    }
	    r = 0.4 * (float) (top - bottom);

	    x -= Ox;		/*Offset x and y around trackball*/
	    y -= Oy;

	    while (Mouse(&newX, &newY))
	    {
		newX -= Ox;		/*Offset the new x and y*/
		newY -= Oy;
	    
		if (ABS(newX - x) >= MINROT || ABS(newY - y) >= MINROT)
		{
		    /*Now we have a differential*/
		    real dr;			/*Click axis distance*/
		    real sw, cw;		/*Sin and cosine of omega*/
		    real st, ct;		/*Sin and cosine of theta*/
		    real sp, cp;		/*Sin and cosine of phi*/
		    real a1x, a1y, a1z;		/*Temporary values for axis of rotation*/
		    real a2x, a2y, a2z;		/*Temporary values for axis of rotation*/
		    real t;			/*1-cos phi*/

		    dr = rsqrt(SQUARE((float) x) +
			       SQUARE((float) y));
		    omega = f(dr / r);

		    /*Calculate theta*/
		    theta = ratan2((float) (y), (float) (x));

		    /*Calculate tao as offset from theta*/
		    tao = ratan2((float) (newY - y), (float) (newX - x)) - theta;

		    /*Calculate phi simplistically*/
		    phi = rsqrt(SQUARE((float) (newX - x)) +
				SQUARE((float) (newY - y))) / r;

		    /*Calculate sin and cos of the angles for speed*/
		    sw = rsin(omega);
		    cw = rcos(omega);
		    st = rsin(theta);
		    ct = rcos(theta);
		    sp = rsin(phi);
		    cp = rcos(phi);
		    t = 1.0 - cp;

		    /*Calculate the axis of rotation*/

		    /*First the motion from origin component*/
		    a1x = -rsin(tao);
		    a1y = rcos(tao);
		    a1z = 0.0;

		    /*Now multiply in the "on x-axis" component*/
		    a2x = a1x * cw + a1z * sw;
		    a2y = a1y;
		    a2z = a1x * -sw + a1z * cw;

		    /*Now multiply in the "from x-axis" component*/
		    ax = a2x * ct + a2y * -st;
		    ay = a2x * st + a2y * ct;
		    az = a2z;

		    /*Calculate the phi and delta time*/
		    if (firstRun)
		    {
			firstRun = false;
			cumPhi = 0.0;
			lastTime = Clock();
		    }
		    else
		    {
			curTime = Clock();
			cumPhi += phi;
			if (curTime > lastTime + MINROTTIME)
			{
			    motorOn = true;
			    rotSpeed = cumPhi / (curTime - lastTime);
			    lastTime = curTime;
			    cumPhi = 0.0;
			}
		    }

		    /*Calculate axis based on combinations of
		      forward, up, and side vectors*/

		    axis[0] = 0.0;
		    axis[1] = 0.0;
		    axis[2] = 0.0;

		    axis[0] += ax * side[0];
		    axis[1] += ax * side[1];
		    axis[2] += ax * side[2];

		    axis[0] += ay * up[0];
		    axis[1] += ay * up[1];
		    axis[2] += ay * up[2];

		    axis[0] -= az * forward[0];
		    axis[1] -= az * forward[1];
		    axis[2] -= az * forward[2];

		    /*Now that there's a new delta, save it and redraw*/
		    if (lights)
		    {
			RotateLights(lights, axis, phi, flags & F_SHIFTDOWN ? true : false, observer, object);
		    }
		    else
		    {
			RotateObserver(observer, axis, phi, flags & F_SHIFTDOWN ? true : false);
		    }

		    x = newX;
		    y = newY;
		    ImInvalid(observer);
		    DrawMe(object);
		}
		else
		{
		    firstRun = true;
		    motorOn = false;
		}
	    }
	    SetVar(object, ROTATING, false);
	}

	if (logging)
	{
	    char cmd[256];
	    char *s;
	    Bool rotLights;
	    ThingListPtr runner;

	    /*See if it's lights that were rotated*/
	    rotLights = false;
	    if (lights)
	    {
		runner = LISTOF(lights);

		while (runner)
		{
		    if (IsSelected(runner -> thing))
		    {
			/*Rotated light*/
	    
			ObjPtr var;
			real location[3];
			    
			rotLights = true;
			var = GetFixedArrayVar("RotateSpace", runner -> thing, LOCATION, 1, 3L);
			if (var)
			{
			    Array2CArray(location, var);
			    sprintf(cmd, "set location ");
			    s = cmd;
			    while (*s) ++s;
			    MakeObjectName(s, runner -> thing);
			    while (*s) ++s;
			    sprintf(s, " [%g %g %g]\n",
				    location[0],
				    location[1],
				    location[2]);
			    Log(cmd);
			}
		    }
		    runner = runner -> next;
		}
	    }
	    else
	    {
		/*No lights rotated*/
		LogObserver(observer);
	    }
	}

	if (!lights)
	{
	    if (GetPrefTruth(PREF_ROTINERTIA) && motorOn)
	    {
		SetRotationMotor((real) rotSpeed, axis[0], axis[1], axis[2]);
	    }
	    else
	    {
		SetRotationMotor((real) 0.0, axis[0], axis[1], axis[2]);
	    }
	}

    }
#endif
    return ObjTrue;
}

static ObjPtr FingerSpace(space, x, y, flags)
ObjPtr space;
int x, y;
long flags;
/*Does a 3-D finger in a space*/
{
    int left, right, bottom, top;
    ObjPtr contents;
    ThingListPtr runner;
    FuncTyp method;

    Get2DIntBounds(space, &left, &right, &bottom, &top);

    StartSpace(space, left, right, bottom, top, PICKSPACE, 0);

    /*Pick the contents of the space*/
    contents = GetVar(space, CONTENTS);

    runner = LISTOF(contents);
    while (runner)
    {
	if (IsObject(runner -> thing))
	{
	    PickObject(runner -> thing, PR_CENTER);
	    method = GetMethod(runner -> thing, PICKPOINT);
	    if (method)
	    {
		(*method)(runner -> thing);
	    }
	    EndPickObject();
	}
	runner = runner -> next;
    }

    StopSpace(PICKSPACE);

    if (0 == (flags & F_EXTEND))
    {
	DeselectAll();
    }

    if (spacePickedObject)
    {
#if 0
	if ((bestPickVertex) && (method = GetMethod(spacePickedObject, 
		SELECTSELPOINT)))
	{
	    if (!IsSelected(spacePickedObject))
	    {
		Select(spacePickedObject, true);
	    }
	    (*method)(spacePickedObject, bestPickVertex, flags);
	}
	else
#endif
	{
	    /*Select or deselect as a whole*/
	    if ((flags & F_EXTEND) && IsSelected(spacePickedObject))
	    {
		Select(spacePickedObject, false);
	    }
	    else
	    {
		Select(spacePickedObject, true);
	    }
	}
	return ObjTrue;
    }
    return ObjFalse;
}

static ObjPtr TurnDialSpace(space, whichDial, delta, flags)
ObjPtr space;
int whichDial;
real delta;
long flags;
/*Turns dial whichDial in space space by delta*/
{
    ObjPtr var;
    ObjPtr observer;
    real axis[3];
    real phi;
    real mv;
    real eyePosn[3];
    real forward[3], up[3], side[3];
    real clips[2];

    phi = delta * DIALROTFACTOR * M_PI_2;
    mv = delta * DIALMOVEFACTOR * M_PI_2;

    observer = GetObjectVar("TurnDialSpace", space, OBSERVER);
    if (observer)
    {
	GetObserverLocation(eyePosn, observer);
	GetAdjustedVectors(forward, up, observer);
	CROSS(up, forward, side);

	switch (whichDial)
	{
	    case 7:		/*Motion in x direction*/
		if (flags & F_OPTIONDOWN)
		{
		    eyePosn[0] -= mv;
		}
		else
		{
		    eyePosn[0] += side[0] * mv;
		    eyePosn[1] += side[1] * mv;
		    eyePosn[2] += side[2] * mv;
		}
		var = NewRealArray(1, 3L);
		CArray2Array(var, eyePosn);
		SetVar(observer, LOCATION, var);
		ImInvalid(observer);
		break;
	    case 6:		/*Rotation about y*/
		if (flags & F_OPTIONDOWN)
		{
		    axis[0] = 0.0;
		    axis[1] = 1.0;
		    axis[2] = 0.0;
		    RotateObserver(observer, axis, phi, false);
		}
		else
		{
		    RotateObserver(observer, up, phi, false);
		}
		break;
	    case 5:		/*Motion in y direction*/
		if (flags & F_OPTIONDOWN)
		{
		    eyePosn[1] -= mv;
		}
		else
		{
		    eyePosn[0] -= up[0] * mv;
		    eyePosn[1] -= up[1] * mv;
		    eyePosn[2] -= up[2] * mv;
		}
		var = NewRealArray(1, 3L);
		CArray2Array(var, eyePosn);
		SetVar(observer, LOCATION, var);
		ImInvalid(observer);
		break;
	    case 4:		/*Rotation to top/bottom*/
		if (flags & F_OPTIONDOWN)
		{
		    axis[0] = -1.0;
		    axis[1] = 0.0;
		    axis[2] = 0.0;
		    RotateObserver(observer, axis, phi, false);
		}
		else
		{
		    RotateObserver(observer, side, phi, false);
		}
		break;
	    case 3:		/*Motion in z direction, or forward*/
		if (flags & F_OPTIONDOWN)
		{
		    eyePosn[2] -= mv;
		}
		else
		{
		    eyePosn[0] += forward[0] * mv;
		    eyePosn[1] += forward[1] * mv;
		    eyePosn[2] += forward[2] * mv;

		    var = GetVar(observer, FOCUSDIST);
		    if (var)
		    {
			SetVar(observer, FOCUSDIST, NewReal(GetReal(var) - mv));
		    }
		    var = GetVar(observer, VIEWCLIP);
		    if (var)
		    {
			Array2CArray(clips, var);
			clips[0] -= mv;
			clips[1] -= mv;
			var = NewRealArray(1, 2L);
			CArray2Array(var, clips);
			SetVar(observer, VIEWCLIP, var);
		    }
		}
		var = NewRealArray(1, 3L);
		CArray2Array(var, eyePosn);
		SetVar(observer, LOCATION, var);
		ImInvalid(observer);
		break;
	    case 2:		/*Rotation about z*/
		if (flags & F_OPTIONDOWN)
		{
		    axis[0] = 0.0;
		    axis[1] = 0.0;
		    axis[2] = 1.0;
		    RotateObserver(observer, axis, -phi, false);
		}
		else
		{
		    RotateObserver(observer, forward, phi, false);
		}
		break;
	    case 0:		/*Change clipping plane*/
		var = GetVar(observer, VIEWCLIP);
		if (var)
		{
		    Array2CArray(clips, var);
		    if (flags & F_OPTIONDOWN)
		    {
			clips[1] += mv;
			if (clips[1] < clips[0] + 0.001) clips[1] = clips[0] + 0.001;
		    }
		    else
		    {
			clips[0] += mv;
			if (clips[1] < clips[0] + 0.001) clips[0] = clips[1] - 0.001;
		    }
		    var = NewRealArray(1, 2L);
		    CArray2Array(var, clips);
		    SetVar(observer, VIEWCLIP, var);
		    ImInvalid(observer);
		}
		break;
	    case 1:		/*Change angle or focus width*/
		if (flags & F_OPTIONDOWN)
		{
		    /*Change binocular spacing*/
		    var = GetVar(observer, BINOCULARITY);
		    if (var)
		    {
			real val;
			val = GetReal(var) + delta * DIALBINOCFACTOR;
			if (val < 0.0) val = 0.0;
			SetVar(observer, BINOCULARITY, NewReal(val));
			ImInvalid(observer);
		    }
		}
		else
		{
		    /*Change angle or field width*/
		    Bool isOrtho = false;

		    MakeVar(observer, VIEWTYPE);
		    var = GetVar(observer, VIEWTYPE);
		    if (var)
		    {
			if (GetInt(var) == VT_ORTHOGRAPHIC)
			{
			    isOrtho = true;
			}
		    }

		    var = GetVar(observer, VIEWFIELD);
		    if (var)
		    {
			if (isOrtho)
			{
			    real val;
			    val = GetReal(var) + delta * DIALWIDTHFACTOR;
			    if (val < 0.001) val = 0.001;
			    SetVar(observer, VIEWFIELD, NewReal(val));
			}
			else
			{
			    real val;
			    val = GetReal(var) + delta * DIALANGLEFACTOR * 180.0;
			    if (val < 0.1) val = 0.1;
			    if (val > 179.0) val = 179.0;
			    SetVar(observer, VIEWFIELD, NewReal(val));
			}
			ImInvalid(observer);
		    }
		}
		break;
	}
    }
    return ObjTrue;
}

static ObjPtr PressSpace(object, x, y, flags)
ObjPtr object;
int x, y;
long flags;
/*Does a press in a space beginning at x and y.  Returns
  true iff the press really was in the space.*/
{
#ifdef INTERACTIVE
    int left, right, bottom, top;
    ObjPtr tool;
    int spaceTool;
 
    /*Get the space tool*/
    tool = GetIntVar("PressSpace", object, EDITTOOL);
    if (!tool)
    {
	return ObjFalse;
    }
    spaceTool = GetInt(tool);

    Get2DIntBounds(object, &left, &right, &bottom, &top);

    if (x >= left && x <= right && y >= bottom && y <= top)
    {
	if (TOOL(flags) == T_HELP)
	{
	    ContextHelp(object);
	    return ObjTrue;
	}

	/*It's a click in the space*/
	switch (spaceTool)
	{
	    case ST_FLYING:
		{
		    /*Toggle flying*/
		    Bool flying;
		    ObjPtr observer;
		    MakeMeCurrent(object);

		    observer = GetObjectVar("PressSpace", object, OBSERVER);
		    if (!observer) return ObjFalse;
	    
		    flying = GetPredicate(observer, FLYING);
		    flying = flying ? false : true;
		    if (flying)
		    {
			WakeMe(observer, MARKTIME, Clock() + 0.001);
		    }
		    else
		    {
			DoNotDisturb(observer, MARKTIME);
		    }
	    
		    SetVar(observer, FLYING, flying ? ObjTrue : ObjFalse);
		}
		return ObjTrue;
		break;
	    case ST_FINGER:
		MakeMeCurrent(object);
		if (TOOL(flags) == T_ROTATE)
		{
		    return RotateSpace(object, x, y, flags);
		}
		else
		{
		    ObjPtr test;
		    test = FingerSpace(object, x, y, flags);
		    if (IsTrue(test))
		    {
		        MoveSpace(object, x, y, flags);
			return ObjTrue;
		    }
		}
		break;
	    case ST_METER:
		{
		    ObjPtr test;
		    test = FingerSpace(object, x, y, flags);
		    if (IsTrue(test) && bestPickVertex)
		    {
			ObjPtr var;
			real *elements;

			var = NewRealArray(1, 3L);
			elements = ELEMENTS(var);
			elements[0] = bestPickVertex -> position[0];
			elements[1] = bestPickVertex -> position[1];
			elements[2] = bestPickVertex -> position[2];

			SetVar(object, METERLOCATION, var);

			return ObjTrue;
		    }
		}
		break;
	}
	return ObjFalse;
    }
    else
#endif
    {
	return ObjFalse;
    }
}

int dsn = 0;

#ifdef HAVE_PROTOTYPES
void DrawSpaceContents(ObjPtr space, int left, int right, int bottom, int top, int viewType)
#else
void DrawSpaceContents(space, left, right, bottom, top, viewType)
ObjPtr space;
int left, right, bottom, top;
int viewType;
#endif
{
#ifdef GRAPHICS
    ObjPtr contents;
    Bool anyTransparent;		/*True iff any objects are transparent*/
    ThingListPtr drawList;		/*List of objects to draw*/
    ObjPtr frontPanel;
    int prefDrawMoving;			/*Preferred draw moving style*/
    ObjPtr var;
    Bool isCached;
    ObjPtr cache;


    if (windowSystem == WS_GLWINDOWS)
    {
	frontPanel = GetVar(space, FRONTPANEL);
	if (frontPanel &&
	    (GetVar(frontPanel, BACKGROUND) || GetPredicate(frontPanel, TEMPOBSCURED)))
	{
	    /*Front panel has a background.  Don't need to draw space*/
	    return;
	}
    }

    /*Set a new viewport to the current area*/
    StartSpace(space, left, right, bottom, top, DRAWSPACE, viewType);

    /*Figure out the range of drawing qualities to use.*/
    if (interactiveMoving)
    {
	/*It's interactive.  Figure out what quality to use*/
	prefDrawMoving = GetPrefInteger(PREF_DRAWMOVING);

	if (prefDrawMoving == DM_SKELETON)
	{
	    minDrawingQuality = DQ_MIN;
	    maxDrawingQuality = DQ_OUTLINE;
	}
	else if (prefDrawMoving == DM_FULL)
	{
	    minDrawingQuality = DQ_MIN;
	    maxDrawingQuality = DQ_MAX;
	}
	else if (prefDrawMoving == DM_AUTO)
	{
	    var = GetVar(space, DRAWINGTIME);
	    if (var)
	    {
		long drawingTime;
		/*There is a drawing time recorded.  Get it*/
		drawingTime = GetInt(var);

		if (drawingTime < MAXMOVETIME)
		{
		    /*It's fast enough, might as well do it fully*/
		    minDrawingQuality = DQ_MIN;
		    maxDrawingQuality = DQ_MAX;
		}
		else
		{
		    /*Do outline*/
		    minDrawingQuality = DQ_MIN;
		    maxDrawingQuality = DQ_OUTLINE;
		}
	    }
	    else
	    {
		/*Assume we have to do full drawing*/
		minDrawingQuality = DQ_MIN;
		maxDrawingQuality = DQ_MAX;
	    }
	}
    }
    else
    {
	/*Not interactive.  Just use full quality*/
	minDrawingQuality = DQ_MIN;
	maxDrawingQuality = DQ_MAX;
    }

    /*Draw the contents of the space*/
    contents = GetVar(space, CONTENTS);

    /*First draw opaque objects*/
    anyTransparent = false;
    drawList = LISTOF(contents);
    drawingTransparent = false;
    while (drawList)
    {
	if (IsObject(drawList -> thing))
	{
	    if (GetPredicate(drawList -> thing, ISTRANSPARENT))
	    {
		anyTransparent = true;
	    }
	    qualityDrawnSoFar = DQ_MIN;

	    if (maxDrawingQuality >= DQ_FULL && 
		GetPredicate(drawList -> thing, CACHEGRAPHICS))
	    {
		cacheMade = false;
		MakeVar(drawList -> thing, CACHEDOPAQUE);
		if (cacheMade)
		{
		    cache = GetVar(drawList -> thing, CACHEDOPAQUE);
		    if (cache)
		    {
			OpenGraphObj(cache);
		    }
		    else
		    {
			cache = NewOpenGraphObj();
			SetVar(drawList -> thing, CACHEDOPAQUE, cache);
		    }
		    DrawVisObject(drawList -> thing);
		    CloseGraphObj(cache);
		    DrawGraphObj(cache);
		}
		else
		{
		    /*Just draw it*/
		    cache = GetVar(drawList -> thing, CACHEDOPAQUE);
		    if (cache)
		    {
			DrawGraphObj(cache);
		    }
		    else
		    {
			DrawVisObject(drawList -> thing);
		    }
		}
	    }
	    else
	    {
		DrawVisObject(drawList -> thing);
	    }
	    if (maxDrawingQuality < DQ_FULL)
	    {
		NeedToDrawFullQuality(drawList -> thing);
	    }
	}
	drawList = drawList -> next;
    }

    /*Now draw transparent objects*/
    if (anyTransparent && rgbp && maxDrawingQuality >= DQ_FULL)
    {
	BeginMask(true, true, true, false);
	blendfunction(BF_ONE, BF_ONE);

	drawingTransparent = true;
	drawList = LISTOF(contents);
	while (drawList)
	{
	    minDrawingQuality = DQ_FULL;	
	    maxDrawingQuality = DQ_FULL;
	    if (IsObject(drawList -> thing))
	    {
		qualityDrawnSoFar = DQ_MIN;
		if (maxDrawingQuality >= DQ_FULL && 
		    GetPredicate(drawList -> thing, CACHEGRAPHICS))
		{
		    cacheMade = false;
		    MakeVar(drawList -> thing, CACHEDTRANSPARENT);
		    if (cacheMade)
		    {
			cache = GetVar(drawList -> thing, CACHEDTRANSPARENT);
			if (cache)
			{
			    OpenGraphObj(cache);
			}
			else
			{
			    cache = NewOpenGraphObj();
			    SetVar(drawList -> thing, CACHEDTRANSPARENT, cache);
			}
			DrawVisObject(drawList -> thing);
			CloseGraphObj(cache);
			DrawGraphObj(cache);
		    }
		    else
		    {
			/*Just draw it*/
			cache = GetVar(drawList -> thing, CACHEDTRANSPARENT);
			if (cache)
			{
			    DrawGraphObj(cache);
			}
			else
			{
			    DrawVisObject(drawList -> thing);
			}
		    }
		}
		else
		{
		    DrawVisObject(drawList -> thing);
		}
		if (maxDrawingQuality < DQ_FULL)
		{
		    NeedToDrawFullQuality(drawList -> thing);
		}
	    }
	    drawList = drawList -> next;
	}
	blendfunction(BF_ONE, BF_ZERO);
	EndMask();
	drawingTransparent = false;
    }

    if (maxDrawingQuality >= DQ_FULL)
    {
	/*End the timing of the full drawing*/
	SetVar(space, DRAWINGTIME, NewInt(GetPictureTime()));
    }

    StopSpace(DRAWSPACE);
#endif
}

#ifdef HAVE_PROTOTYPES
Bool RotateObserver(ObjPtr observer, real axis[3], real phi, Bool constrain)
#else
Bool RotateObserver(observer, axis, phi, constrain)
ObjPtr observer;
real axis[3];
real phi;
Bool constrain;
#endif
/*Rotates an observer by phi around axis.  If constrain, constrains axis as
  a side effect.*/
{
    ObjPtr var;
    real focusPoint[3];
    real location[3];
    real forwardVector[3];
    real focusDist;
    Quaternion rotQuat;
    Quaternion deltaRotQuat;
    Quaternion newRot;

    /*Now that there's a new delta, rotate and redraw*/
    if (constrain)
    {
	int k, best;
	float curDot, maxDot = 0.0;
	float rr;

	/*Constrain the axis to the nearest ortho axis*/
	for (k = 0; k < 3; ++k)
	{
	    curDot =
		axis[0] * Identity[k][0] +
		axis[1] * Identity[k][1] +
		axis[2] * Identity[k][2];

	    if (ABS(curDot) > ABS(maxDot))
	    {
		/*It's a better choice*/
		maxDot = curDot;
		best = k;
	    }
	}

	/*Now we have a best match*/
	axis[0] = Identity[best][0];
	axis[1] = Identity[best][1];
	axis[2] = Identity[best][2];
	if (maxDot < 0.0)
	{
	    axis[0] = -axis[0];
	    axis[1] = -axis[1];
	    axis[2] = -axis[2];
	}

	/*Normalize the axis*/
	rr = 1.0 / sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
	axis[0] *= rr;
	axis[1] *= rr;
	axis[2] *= rr;
    }

    /*Get current focus point*/
    GetFocusPoint(focusPoint, observer);

    /*Now make the change rotation quaternion*/
    RotToQuaternion(axis, phi, deltaRotQuat);

    /*Apply it to the rotation quaternion*/
    MakeVar(observer, ROTQUAT);
    var = GetVar(observer, ROTQUAT);
    if (!var)
    {
	ReportError("RotateObserver", "No rotation quaternion");
	return;
    }
    Array2CArray(rotQuat, var);

    QMult(rotQuat, deltaRotQuat, newRot);

    var = NewRealArray(1, 4L);
    CArray2Array(var, newRot);
    SetVar(observer, ROTQUAT, var);

    /*Get new forward vector*/
    GetForwardVector(forwardVector, observer);

    var = GetVar(observer, FOCUSDIST);
    if (var)
    {
	focusDist = GetReal(var);
    }
    else
    {
	focusDist = INITEYEDIST;
    }

    /*Make a new location*/
    location[0] = focusPoint[0] - forwardVector[0] * focusDist;
    location[1] = focusPoint[1] - forwardVector[1] * focusDist;
    location[2] = focusPoint[2] - forwardVector[2] * focusDist;

    /*Put back the location vector*/
    var = NewRealArray(1, 3L);
    CArray2Array(var, location);
    SetVar(observer, LOCATION, var);

    ImInvalid(observer);
    return true;
}

void PrintMatrix(real m[4][4])
/*Prints a matrix*/
{
    printf("%10g %10g %10g %10g\n", m[0][0], m[0][1], m[0][2], m[0][3]);
    printf("%10g %10g %10g %10g\n", m[1][0], m[1][1], m[1][2], m[1][3]);
    printf("%10g %10g %10g %10g\n", m[2][0], m[2][1], m[2][2], m[2][3]);
    printf("%10g %10g %10g %10g\n", m[3][0], m[3][1], m[3][2], m[3][3]);
}

#ifdef HAVE_PROTOTYPES
void RotToQuaternion(real axis[3], real amount, Quaternion r)
#else
void RotToQuaternion(axis, amount, r)
real axis[3];
real amount;
Quaternion r;
#endif
/*Converts a rotation by amount around axis to a quaternion*/
{
    real absVal, multiplier1, multiplier2;

    absVal = sqrt(SQUARE(axis[0]) + SQUARE(axis[1]) + SQUARE(axis[2]));
    if (absVal > 0.0)
    {
	multiplier1 = rsin(amount * 0.5) / absVal;
	multiplier2 = rcos(amount * 0.5);

	r[0] = multiplier2;
	r[1] = axis[0] * multiplier1;
	r[2] = axis[1] * multiplier1;
	r[3] = axis[2] * multiplier1;
    }
    else
    {
	r[0] = 1.0;
	r[1] = 0.0;
	r[2] = 0.0;
	r[3] = 0.0;
    }
}

#ifdef HAVE_PROTOTYPES
void QuaternionToRot(Quaternion r, real axis[3], real *amount)
#else
void QuaternionToRot(r, axis, amount)
real axis[3];
real *amount;
Quaternion r;
#endif
/*Converts a quaternion to a rotation by amount around axis*/
{
    real absVal, multiplier1, multiplier2;

    absVal = sqrt(SQUARE(r[1]) + SQUARE(r[2]) + SQUARE(r[3]));
    if (absVal > 0.0)
    {
	*amount = racos(r[0]) * 2;
	axis[0] = r[1] / absVal;
	axis[1] = r[2] / absVal;
	axis[2] = r[3] / absVal;
    }
    else
    {
	*amount = 0;
	axis[0] = 0.0;
	axis[1] = 0.0;
	axis[2] = 1.0;
    }
}

#ifdef HAVE_PROTOTYPES
void MatrixToQuaternion(real m[3][3], Quaternion q)
#else
void MatrixToQuaternion(m, q)
real m[3][3];
Quaternion q;
#endif
/*Converts a matrix (rotation only) to a quaternion*/
{
    real greatestSquare;
    char whichGreatest;
    real square;
    real dividend;

#undef w
#undef x
#undef y
#undef z
#define w q[0]
#define x q[1]
#define y q[2]
#define z q[3]

    /*Solving along diagonal, see which is greatest*/
    greatestSquare = 0.0;
    whichGreatest = ' ';

    /*Test solving for x*/
    if ((square = 1.0 - m[2][2] + m[0][0] - m[1][1]) > greatestSquare)
    {
	greatestSquare = square;
	whichGreatest = 'x';
    }

    /*Test solving for y*/
    if ((square = 1.0 - m[0][0] + m[1][1] - m[2][2]) > greatestSquare)
    {
	greatestSquare = square;
	whichGreatest = 'y';
    }

    /*Test solving for z*/
    if ((square = 1.0 - m[1][1] + m[2][2] - m[0][0]) > greatestSquare)
    {
	greatestSquare = square;
	whichGreatest = 'z';
    }

    if (whichGreatest == 'x')
    {
	/*Arbitrarily choose +x*/
	x = sqrt(greatestSquare) * 0.5;
	dividend = 1.0 / (4.0 * x);

	/*Calculate y*/
	y = (m[0][1] + m[1][0]) * dividend;

	/*Calculate z*/
	z = (m[0][2] + m[2][0]) * dividend;

	/*Calculate w*/
	w = 2.0 * (2.0 * y * z - m[2][1]) * dividend; 
    }
    else if (whichGreatest == 'y')
    {
	/*Arbitrarily choose +y*/
	y = sqrt(greatestSquare) * 0.5;
	dividend = 1.0 / (4.0 * y);

	/*Calculate x*/
	x = (m[0][1] + m[1][0]) * dividend;

	/*Calculate z*/
	z = (m[2][1] + m[1][2]) * dividend;

	/*Calculate w*/
	w = 2.0 * (2.0 * x * z - m[0][2]) * dividend; 
    }
    else if (whichGreatest == 'z')
    {
	/*Arbitrarily choose +z*/
	z = sqrt(greatestSquare) * 0.5;
	dividend = 1.0 / (4.0 * z);

	/*Calculate x*/
	x = (m[0][2] + m[2][0]) * dividend;

	/*Calculate y*/
	y = (m[2][1] + m[1][2]) * dividend;

	/*Calculate w*/
	w = 2.0 * (2.0 * x * y - m[1][0]) * dividend; 
    }
    else
    {
	/*It has to be [1, [0 0 0]]*/
	q[0] = 1.0;
	q[1] = 0.0;
	q[2] = 0.0;
	q[3] = 0.0;
    }
#undef w
#undef x
#undef y
#undef z
}

void QInvert(Quaternion s, Quaternion r)
/*Inverts quaternion s into result r, assuming |s| == 1*/
{
    r[0] = s[0];
    r[1] = -s[1];
    r[2] = -s[2];
    r[3] = -s[3];
}

void QMult(Quaternion q1, Quaternion q2, Quaternion r)
/*Multiplies two quaternions giving a third*/
{
    real qp[3];

    r[0] = q1[0] * q2[0] - (q1[1] * q2[1] + q1[2] * q2[2] + q1[3] * q2[3]);

    CROSS(&(q1[1]), &(q2[1]), qp);
    r[1] = q1[0] * q2[1] + q2[0] * q1[1] + qp[0];
    r[2] = q1[0] * q2[2] + q2[0] * q1[2] + qp[1];
    r[3] = q1[0] * q2[3] + q2[0] * q1[3] + qp[2];
}

void QDouble(Quaternion q1, Quaternion q2, Quaternion r)
/*Does the Double operation, but not like [Shoemake, 1985]*/
{
    Quaternion qtemp1, qtemp2;

#if 0
    /*This is from [Shoemake, 1985].  I don't understand the logic, and
	also, it doesn't work.*/

    QMult(q1, q2, qtemp1);
    QMult(qtemp1, q2, qtemp2);

    r[0] = qtemp2[0] * 2.0 - q1[0];
    r[1] = qtemp2[1] * 2.0 - q1[1];
    r[2] = qtemp2[2] * 2.0 - q1[2];
    r[3] = qtemp2[3] * 2.0 - q1[3];
#else
    /*This, however, works fine*/
    QInvert(q1, qtemp1);
    QMult(q2, qtemp1, qtemp2);
    QMult(qtemp2, q2, r);
#endif
}

void Slerp(Quaternion q1, Quaternion q2, real u, Quaternion r)
/*Does the slerp operation in [Shoemake 1985]*/
{
    real theta, costheta, w1, w2, sintheta;

    costheta = q1[0] * q2[0] + q1[1] * q2[1] + q1[2] * q2[2] + q1[3] * q2[3];
    theta = racos(costheta);
    sintheta = rsin(theta);
    if (sintheta > 0.0)
    {
	w1 = rsin((1.0 - u) * theta) / sintheta;
	w2 = rsin(u * theta) / sintheta;
    }
    else
    {
	/*They're the same quaternion, so who cares?*/
	w1 = 1.0;
	w2 = 0.0;
    }

    r[0] = w1 * q1[0] + w2 * q2[0];
    r[1] = w1 * q1[1] + w2 * q2[1];
    r[2] = w1 * q1[2] + w2 * q2[2];
    r[3] = w1 * q1[3] + w2 * q2[3];
}

#ifdef HAVE_PROTOTYPES
void QBisect(Quaternion q1, Quaternion q2, Quaternion r)
#else
void QBisect(q1, q2, r)
Quaternion q1, q2, r;
#endif
/*Does the Bisect operation in [Shoemake, 1985]*/
{
    real absVal;

#if 1
    r[0] = q1[0] + q2[0];
    r[1] = q1[1] + q2[1];
    r[2] = q1[2] + q2[2];
    r[3] = q1[3] + q2[3];

    absVal = sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3]);
    r[0] /= absVal;
    r[1] /= absVal;
    r[2] /= absVal;
    r[3] /= absVal;
#else
    Slerp(q1, q2, 0.5, r);
#endif
}

#ifdef HAVE_PROTOTYPES
void QRot(real v[3], Quaternion q, real r[3])
#else
void QRot(v, q, r)
real v[3];
Quaternion q;
real r[3];
#endif
/*Rotates v by q into r*/
{
    Quaternion qInv, qTemp;
    Quaternion qVec, qRes;

    QInvert(q, qInv);
    qVec[0] = 0.0;
    qVec[1] = v[0];
    qVec[2] = v[1];
    qVec[3] = v[2];

    QMult(qInv, qVec, qTemp);
    QMult(qTemp, q, qRes);

    r[0] = qRes[1];
    r[1] = qRes[2];
    r[2] = qRes[3];
}

void QDelta(Quaternion q1, Quaternion q2, Quaternion r)
/*Put in r the quaternion such that q1 r = q2*/
{
    Quaternion q1inv;

    QInvert(q1, q1inv);
    QMult(q1inv, q2, r);
}

TestObserver()
/*Does an observer test in the current window*/
{
    ObjPtr observer;
    real *forward, *up;

    if (selWinInfo)
    {
	observer = FindObserver(selWinInfo);
	if (observer)
	{
	    ObjPtr var;
	    real *elements;
	    real side[3], m[4][4];
	    Quaternion q, qInv, qTest, qVec;

	    var = GetVar(observer, LOCATION);
	    if (var)
	    {
		forward = (real *) ELEMENTS(var);
		printf("Location = [%g %g %g]\n",
			forward[0], forward[1], forward[2]);
	    }

	    var = GetVar(observer, FORWARDVECTOR);
	    if (var)
	    {
		forward = (real *) ELEMENTS(var);
		printf("Forward = [%g %g %g]\n",
			forward[0], forward[1], forward[2]);
	    }

	    var = GetVar(observer, UPVECTOR);
	    if (var)
	    {
		up = (real *) ELEMENTS(var);
		printf("Up = [%g %g %g]\n",
			up[0], up[1], up[2]);
	    }
	    /* generate the vector side from the
	     * 2 vectors view forward and world up 
	     */
	    CROSS(forward, up, side);
	    NORMALIZE(side);

	    m[0][0] = side[0];
	    m[1][0] = side[1];
	    m[2][0] = side[2];
	    m[3][0] = 0.0;

	    m[0][1] = up[0];
	    m[1][1] = up[1];
	    m[2][1] = up[2];
	    m[3][1] = 0.0;

	    m[0][2] = -forward[0];
	    m[1][2] = -forward[1];
	    m[2][2] = -forward[2];
	    m[3][2] = 0.0;

	    m[0][3] = 0.0;
	    m[1][3] = 0.0;
	    m[2][3] = 0.0;
	    m[3][3] = 1.0;

#if 0
	    printf("Lookat matrix = \n");
	    PrintMatrix(m);

	    /*Now try to make quaternion*/
	    MatrixToQuaternion(m, q);
	    printf("Rotation quaternion = %g %g %g %g\n",
			q[0], q[1], q[2], q[3]);
	    printf("Absolute value squared = %g\n",
			q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
	    QInvert(q, qInv);
	    printf("Inverted = %g %g %g %g\n",
			qInv[0], qInv[1], qInv[2], qInv[3]);

	    qVec[0] = 0.0;
	    qVec[1] = 0.0;
	    qVec[2] = 0.0;
	    qVec[3] = -1.0;
	    QMult(qInv, qVec, qTest);
	    QMult(qTest, q, qVec);
	    printf("Rotated forward = %g %g %g %g\n",
			qVec[0], qVec[1], qVec[2], qVec[3]);
	    qVec[0] = 0.0;
	    qVec[1] = 0.0;
	    qVec[2] = 1.0;
	    qVec[3] = 0.0;
	    QMult(qInv, qVec, qTest);
	    QMult(qTest, q, qVec);
	    printf("Rotated up = %g %g %g %g\n",
			qVec[0], qVec[1], qVec[2], qVec[3]);
#endif
	}
    }
}

#ifdef HAVE_PROTOTYPES
Bool RotateLights(ObjPtr lights, real startAxis[3], real phi, Bool constrain, ObjPtr observer, ObjPtr space)
#else
Bool RotateLights(lights, axis, phi, constrain, observer, space)
ObjPtr lights;
real startAxis[3];
real phi;
Bool constrain;
ObjPtr observer;
ObjPtr space;
#endif
/*Rotates lights by rotDelta wrt observer within space*/
{
    ThingListPtr runner;
    Matrix rotDelta;
    real axis[3];
    float t;
    float sp, cp;		/*Sin and cosine of phi*/

    runner = LISTOF(lights);
    while (runner)
    {
	if (IsSelected(runner -> thing))
	{
	    /*Rotate the light*/
	    ObjPtr var;
	    real location[3];
	    real newLoc[3];
	    float tv[3];
	    Quaternion qRot, qInv;

	    axis[0] = startAxis[0];
	    axis[1] = startAxis[1];
	    axis[2] = startAxis[2];

	    var = GetFixedArrayVar("DrawSpace", runner -> thing, LOCATION, 1, 3L);
	    if (!var)
	    {
		return false;
	    }

	    Array2CArray(location, var);

	    if (GetPredicate(runner -> thing, LIGHTBYOBSERVER))
	    {
		var = GetVar(observer, ROTQUAT);
		if (var)
		{
		    Array2CArray(qRot, var);
		    QRot(location, qRot, location);
		}
	    }

	    /*Test elevation*/
	    tv[0] = location[1];
	    tv[1] = -location[0];
	    tv[2] = 0.0;
   
	    if (constrain)
	    {
		float dot1, dot2;
		float rr;

		/*Constrain the axis to the azimuth or elevation*/

		/*Test azimuth*/
		dot1 = axis[2];

		/*Test elevation*/
		rr = sqrt(tv[0] * tv[0] + tv[1] * tv[1] + tv[2] * tv[2]);
		if (rr > 0.0)
		{
		    rr = 1.0 / rr;
		    tv[0] *= rr;
		    tv[1] *= rr;
		    tv[2] *= rr;
		}
		else
		{
		    tv[0] = 1.0;
		    tv[1] = 0.0;
		    tv[2] = 0.0;
		}

		dot2 = axis[0] * tv[0] +
		       axis[1] * tv[1] +
		       axis[2] * tv[2];

		if (ABS(dot1) > ABS(dot2))
		{
		    /*Azimuth is better*/
		    axis[0] = 0.0;
		    axis[1] = 0.0;
		    axis[2] = 1.0;
		    if (dot1 < 0.0)
		    {
			axis[0] = -axis[0];
			axis[1] = -axis[1];
			axis[2] = -axis[2];
		    }
		}
	        else
		{
		    axis[0] = tv[0];
		    axis[1] = tv[1];
		    axis[2] = tv[2];
		    /*Elevation is better*/
		    if (dot2 < 0.0)
		    {
			axis[0] = -axis[0];
			axis[1] = -axis[1];
			axis[2] = -axis[2];
		    }
		}
	    }

	    sp = rsin(phi);
	    cp = rcos(phi);
	    t = 1.0 - cp;

	    /*Now make the change rotation matrix by rows from top to bottom*/
	    rotDelta[0][0] = t * SQUARE(axis[0]) + cp;
	    rotDelta[0][1] = t * axis[0] * axis[1] + sp * axis[2];
	    rotDelta[0][2] = t * axis[0] * axis[2] - sp * axis[1];
	    rotDelta[0][3] = 0.0;

	    rotDelta[1][0] = t * axis[0] * axis[1] - sp * axis[2];
	    rotDelta[1][1] = t * SQUARE(axis[1]) + cp;
	    rotDelta[1][2] = t * axis[1] * axis[2] + sp * axis[0];
	    rotDelta[1][3] = 0.0;

	    rotDelta[2][0] = t * axis[0] * axis[2] + sp * axis[1];
	    rotDelta[2][1] = t * axis[1] * axis[2] - sp * axis[0];
	    rotDelta[2][2] = t * SQUARE(axis[2]) + cp;
	    rotDelta[2][3] = 0.0;

	    rotDelta[3][0] = 0.0;
	    rotDelta[3][1] = 0.0;
	    rotDelta[3][2] = 0.0;
	    rotDelta[3][3] = 1.0;

	    /*Rotate this location by rotDelta*/
	    newLoc[0] = rotDelta[0][0] * location[0] +
			rotDelta[1][0] * location[1] +
			rotDelta[2][0] * location[2];
	    newLoc[1] = rotDelta[0][1] * location[0] +
			rotDelta[1][1] * location[1] +
			rotDelta[2][1] * location[2];
	    newLoc[2] = rotDelta[0][2] * location[0] +
			rotDelta[1][2] * location[1] +
			rotDelta[2][2] * location[2];
	    location[0] = newLoc[0];
	    location[1] = newLoc[1];
	    location[2] = newLoc[2];

	    NORM3(newLoc);

	    if (GetPredicate(runner -> thing, LIGHTBYOBSERVER))
	    {
		QInvert(qRot, qInv);
		QRot(newLoc, qRot, newLoc);
	    }

	    var = NewRealArray(1, 3L);
	    CArray2Array(var, newLoc);
	    SetVar(runner -> thing, LOCATION, var);
	    ImInvalid(runner -> thing);
	}
	runner = runner -> next;
    }
    return true;
}

int spaceDraw = 1;

ObjPtr SpacePanelBoundsInvalid(object, changeCount)
ObjPtr object;
unsigned long changeCount;
/*For a space panel, tests to see if it needs drawing.  Returns
  NULLOBJ	if it does not
  array[4]	giving bounds if it does
  ObjTrue	it it needs to be redrawn but does not know where

  Contents are assumed to be shifted in the bounds of owner
*/
{
    ObjPtr contents;
    ObjPtr myBounds;
    real boundsElements[4];

    MakeVar(object, APPEARANCE);

    MakeVar(object, CHANGEDBOUNDS);
    if (GetVarChangeCount(object, CHANGEDBOUNDS) > changeCount)
    {
	/*Object is not good, so return the bounds*/

	myBounds = GetVar(object, CHANGEDBOUNDS);
	return myBounds;
    }

    myBounds = GetVar(object, BOUNDS);

    MakeVar(object, CONTENTS);
    contents = GetVar(object, CONTENTS);
    if (contents && IsList(contents))
    {
	/*Still, maybe some of the contents need to be drawn*/
	real testElements[4];
	real myBoundsElements[4];
 	ObjPtr test;
 	ThingListPtr runner;

	MakeVar(object, BOUNDS);
	myBounds = GetVar(object, BOUNDS);
	Array2CArray(myBoundsElements, myBounds);

	runner = LISTOF(contents);
	while (runner)
	{
	    test = BoundsInvalid(runner -> thing, changeCount);
	    if (test)
	    {
		/*Hey, the kid needs redrawing*/
		return myBounds;
	    }
	    runner = runner -> next;
	}
    }

    return NULLOBJ;
}

ObjPtr SpaceBoundsInvalid(object, changeCount)
ObjPtr object;
unsigned long changeCount;
/*For a space, tests to see if it needs drawing.  Returns
  NULLOBJ	if it does not
  array[4]	giving bounds if it does
  ObjTrue	it it needs to be redrawn but does not know where
*/
{
    ObjPtr contents;
    ObjPtr lights, observer, renderer;
    ObjPtr myBounds;
    real boundsElements[4];
    real testElements[4];
    ObjPtr test;
    real myBoundsElements[4];
    ObjPtr scrollBar;
    Bool firstTime = true;
    Bool doubleNoBounds = false;
    ObjPtr retVal = NULLOBJ;
    ObjPtr time;

    MakeVar(object, TIME);
    MakeVar(object, APPEARANCE);
    MakeVar(object, BOUNDS);

    myBounds = GetFixedArrayVar("SpaceBoundsInvalid", object, BOUNDS, 1, 4L);
    Array2CArray(myBoundsElements, myBounds);

    if (GetVarChangeCount(object, CHANGEDBOUNDS) > changeCount)
    {
	/*Object is not good, so return the bounds*/
	ObjPtr myBounds;

	MakeVar(object, CHANGEDBOUNDS);
	myBounds = GetVar(object, CHANGEDBOUNDS);

	if (myBounds && IsArray(myBounds) && RANK(myBounds) == 1
		&& DIMS(myBounds)[0] == 4)
	{
	    retVal = myBounds;
	}
	else
	{
	    retVal = ObjTrue;
	}
    }

    time = GetVar(object, TIME);
    if (time)
    {
	spaceTime = GetReal(time);
    }
    SetAllDatasetTimes(spaceTime);

    MakeVar(object, CONTENTS);
    contents = GetVar(object, CONTENTS);
    if (contents && IsList(contents))
    {
	/*Still, maybe some of the contents need to be drawn*/
 	ThingListPtr runner;

	runner = LISTOF(contents);
	while (runner)
	{
	    test = BoundsInvalid(runner -> thing, changeCount);
	    if (test)
	    {
		/*Hey, the kid needs redrawing*/
		retVal = myBounds;
	    }
	    runner = runner -> next;
	}
    }

    MakeVar(object, LIGHTS);
    lights = GetVar(object, LIGHTS);
    if (lights && IsList(lights))
    {
	/*Still, maybe some of the lights need to be drawn*/
 	ThingListPtr runner;

	runner = LISTOF(lights);
	while (runner)
	{
	    test = BoundsInvalid(runner -> thing, changeCount);
	    if (test)
	    {
		/*Hey, the light needs redrawing*/
		retVal =  myBounds;
	    }
	    runner = runner -> next;
	}
    }

    MakeVar(object, OBSERVER);
    observer = GetVar(object, OBSERVER);
    if (observer)
    {
	test = BoundsInvalid(observer, changeCount);
	if (test)
	{
	    /*Hey, the observer needs redrawing*/
	    retVal = myBounds;
	}
    }

    MakeVar(object, RENDERER);
    renderer = GetVar(object, RENDERER);
    if (renderer)
    {
	test = BoundsInvalid(renderer, changeCount);
	if (test)
	{
	    /*Hey, the renderer needs redrawing*/
	    retVal =  myBounds;
	}
    }

    return retVal;
}

ObjPtr DrawSpace(object)
ObjPtr object;
/*Draws a space and everything it contains*/
{
#ifdef GRAPHICS
    int left, right, bottom, top;
    ObjPtr eyePosnObj;			/*Eyeposition object*/
    ObjPtr tempObj;			/*Temporary object*/
    ObjPtr dRotMatrix;
    ObjPtr observer, observers;		/*Observer of the space*/
    ObjPtr panel;			/*Front and back panel*/
    ObjPtr var;				/*A random variable*/
    int renderType, filterType;		/*Types of renderers and filters*/
    int viewType;			/*Type of view*/
    int stereoMode;			/*Stereo mode*/

#if 0
    printf("DrawSpace %d\n", spaceDraw++);
#endif

    /*Get render type*/
    MakeVar(object, RENDERTYPE);
    var = GetIntVar("DrawSpace", object, RENDERTYPE);
    if (var)
    {
	renderType = GetInt(var);
    }
    else
    {
	renderType = hasRGB ? RT_RGB_HARDWARE : RT_CMAP_HARDWARE;
    }
    if (renderType == RT_NONE)
    {
	/*Don't render*/
	return ObjTrue;
    }

    /*Get filter type*/
    MakeVar(object, FILTERTYPE);
    var = GetIntVar("DrawSpace", object, FILTERTYPE);
    if (var)
    {
	filterType = GetInt(var);
    }
    else
    {
	filterType = FT_NONE;
    }

    Get2DIntBounds(object, &left, &right, &bottom, &top);
    if (windowSystem == WS_GLWINDOWS)
    {
	if (IsDrawingRestricted(left, right, bottom, top)) return ObjFalse;
    }

    /*Update the rotation matrix and fly through*/
    observer = GetObjectVar("DrawSpace", object, OBSERVER);

    MakeVar(observer, VIEWTYPE);
    var = GetVar(observer, VIEWTYPE);
    if (var)
    {
	viewType = GetInt(var);
    }
    else
    {
	viewType = VT_PERSPECTIVE;
    }

    MakeVar(observer, STEREOMODE);
    var = GetVar(observer, STEREOMODE);
    if (var)
    {
	stereoMode = GetInt(var);
    }
    else
    {
	stereoMode = SM_MONO;
    }

    if (drawingMode == DRAW_SCREEN)
    {
	switch(stereoMode)
	{
	    case SM_MONO:
		DrawSpaceContents(object, left, right, bottom, top, VIEW_CENTER | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		break;
	    case SM_CROSSEYED:
		DrawSpaceContents(object, left, right, bottom, top, VIEW_XLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		DrawSpaceContents(object, left, right, bottom, top, VIEW_XRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		break;
	    case SM_EVENODD:
		DrawSpaceContents(object, left, right, bottom, top, VIEW_EOLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		DrawSpaceContents(object, left, right, bottom, top, VIEW_EORIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		break;
	    case SM_ODDEVEN:
		DrawSpaceContents(object, left, right, bottom, top, VIEW_OELEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		DrawSpaceContents(object, left, right, bottom, top, VIEW_OERIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		break;
	    case SM_WALLEYED:
		DrawSpaceContents(object, left, right, bottom, top, VIEW_WLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		DrawSpaceContents(object, left, right, bottom, top, VIEW_WRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		break;
	    case SM_LEFTONTOP:
		DrawSpaceContents(object, left, right, bottom, top, VIEW_CLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		DrawSpaceContents(object, left, right, bottom, top, VIEW_CRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		break;
	    case SM_REDCYAN:
		if (rgbp)
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_RCLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_RCRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		else
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_CENTER | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		break;
	    case SM_CYANRED:
		if (rgbp)
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_CRLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_CRRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		else
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_CENTER | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		break;
	    case SM_REDGREEN:
		if (rgbp)
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_RGLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_RGRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		else
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_CENTER | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		break;
	    case SM_GREENRED:
		if (rgbp)
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_GRLEFT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_GRRIGHT | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		else
		{
		    DrawSpaceContents(object, left, right, bottom, top, VIEW_CENTER | (viewType == VT_ORTHOGRAPHIC ? VIEW_MOD_ORTHO : 0));
		}
		break;
	}

	/*Filter space if need be*/
	if (rgbp && drawingQuality == DQ_FULL && filterType == FT_SHRINK)
	{
	    /*Shrink the pixels in the window*/
	    int s, d;
	    register int y, x;
	    int xdd, ydd;
	    int xSize, ySize;
	    Pixel *imageBuffer;
    
	    if (right - left > SCRWIDTH) right = left + SCRWIDTH;
	    if (top - bottom > SCRHEIGHT) top = bottom + SCRHEIGHT;
    
	    if ((right - left) & 1) --right;
	    if ((top - bottom) & 1) --top;
	    xdd = (right - left) / 2;
	    ydd = (top - bottom) / 2;
	    xSize = (right - left);
	    ySize = (top - bottom);
    
	    /*Create an image buffer*/
	    imageBuffer = (Pixel *) Alloc(SCRWIDTH * SCRHEIGHT * sizeof(Pixel));
	    if (imageBuffer)
	    {
		lrectread((Screencoord) left, (Screencoord) bottom, 
			(Screencoord) right - 1, (Screencoord) top - 1, (unsigned long *) imageBuffer);
	
		s = 0;
		d = 0;
		for (y = 0; y <= ydd; ++y)
		{
		    for (x = 0; x <= xdd; ++x)
		    {
			imageBuffer[d] . red =
				(imageBuffer[s] . red +
				 imageBuffer[s + 1] . red +
				 imageBuffer[s + xSize] . red +
				 imageBuffer[s + xSize + 1] . red) / 4;
			imageBuffer[d] . green =
				(imageBuffer[s] . green +
				 imageBuffer[s + 1] . green +
				 imageBuffer[s + xSize] . green +
				 imageBuffer[s + xSize + 1] . green) / 4;
			imageBuffer[d] . blue =
				(imageBuffer[s] . blue +
				 imageBuffer[s + 1] . blue +
				 imageBuffer[s + xSize] . blue +
				 imageBuffer[s + xSize + 1] . blue) / 4;
			imageBuffer[d] . alpha =
				(imageBuffer[s] . alpha +
				 imageBuffer[s + 1] . alpha +
				 imageBuffer[s + xSize] . alpha +
				 imageBuffer[s + xSize + 1] . alpha) / 4;
			s += 2;
			++d;
		    }
		    s += xSize;
		}
		EraseAll();
		lrectwrite((Screencoord) left, (Screencoord) bottom, 
			(Screencoord) left + xdd - 1, (Screencoord) bottom + ydd - 1, 
			(unsigned long *) imageBuffer);
		SAFEFREE(imageBuffer);
	    }
	}
	if (rgbp && drawingQuality == DQ_FULL && filterType == FT_4AVERAGE)
	{
	    /*Average the pixels in the window*/
	    int s;
	    register int y, x;
	    int xSize, ySize;
	    Pixel *imageBuffer;
    
	    if (right - left > SCRWIDTH) right = left + SCRWIDTH;
	    if (top - bottom > SCRHEIGHT) top = bottom + SCRHEIGHT;
    
	    xSize = (right - left);
	    ySize = (top - bottom);
    
	    /*Create an image buffer*/
	    imageBuffer = (Pixel *) Alloc(SCRWIDTH * SCRHEIGHT * sizeof(Pixel));
	    if (imageBuffer)
	    {
		lrectread((Screencoord) left, (Screencoord) bottom, 
			(Screencoord) right - 1, (Screencoord) top - 1, (unsigned long *) imageBuffer);
	
		s = 0;
		for (y = 0; y <= ySize - 1; ++y)
		{
		    for (x = 0; x <= xSize - 1; ++x)
		    {
			imageBuffer[s] . red =
				(imageBuffer[s] . red +
				 imageBuffer[s + 1] . red +
				 imageBuffer[s + xSize] . red +
				 imageBuffer[s + xSize + 1] . red) / 4;
			imageBuffer[s] . green =
				(imageBuffer[s] . green +
				 imageBuffer[s + 1] . green +
				 imageBuffer[s + xSize] . green +
				 imageBuffer[s + xSize + 1] . green) / 4;
			imageBuffer[s] . blue =
				(imageBuffer[s] . blue +
				 imageBuffer[s + 1] . blue +
				 imageBuffer[s + xSize] . blue +
				 imageBuffer[s + xSize + 1] . blue) / 4;
			imageBuffer[s] . alpha = 255;
			++s;
		    }
		    ++s;
		}
		lrectwrite((Screencoord) left, (Screencoord) bottom, 
			(Screencoord) right - 1, (Screencoord) top - 1, (unsigned long *) imageBuffer);
		SAFEFREE(imageBuffer);
	    }
	}
    }
    else if (drawingMode == DRAW_POSTSCRIPT && psFile)
    {
	/*Just save the image to a PostScript file*/
	register int y, x;
	int xSize, ySize;
	Pixel *imageBuffer;

	if (right - left > SCRWIDTH) right = left + SCRWIDTH;
	if (top - bottom > SCRHEIGHT) top = bottom + SCRHEIGHT;

	xSize = (right - left);
	ySize = (top - bottom);

	/*Create an image buffer*/
	imageBuffer = (Pixel *) Alloc(xSize * ySize * sizeof(Pixel));
	if (imageBuffer)
	{
	    lrectread((Screencoord) left, (Screencoord) bottom, 
		    (Screencoord) right - 1, (Screencoord) top - 1, (unsigned long *) imageBuffer);
	    SavePSImage(psFile, imageBuffer, left, right, bottom, top);
	    SAFEFREE(imageBuffer);
	}
    }
#endif
    return ObjTrue;
}

static ObjPtr KeyDownSpace(object, key, flags)
ObjPtr object;
int key;
long flags;
/*Does a keydown in a space.  Returns
  true iff the press really was in the space.*/
{
    ObjPtr retVal, tool, var;

    /*Get the space tool*/
    tool = GetIntVar("KeyDownSpace", object, EDITTOOL);

    if (tool && GetInt(tool) == ST_FLYING)
    {
	ObjPtr observer;

	observer = GetObjectVar("KeyDownSpace", object, OBSERVER);
	if (!observer)
	{
	    return ObjFalse;
	}

	switch(key)
	{
	    case '0':
		SetVar(observer, AIRSPEED, NewReal(0.0));
		return ObjTrue;
	    case FK_UP_ARROW:
		var = GetVar(observer, AIRSPEED);
		if (var)
		{
		    SetVar(observer, AIRSPEED, NewReal(AIRSPEEDFACTOR + GetReal(var)));
		}
		else
		{
		    SetVar(observer, AIRSPEED, NewReal(AIRSPEEDFACTOR));
		}
		return ObjTrue;
	    case FK_DOWN_ARROW:
		var = GetVar(observer, AIRSPEED);
		if (var)
		{
		    SetVar(observer, AIRSPEED, NewReal(GetReal(var) - AIRSPEEDFACTOR));
		}
		else
		{
		    SetVar(observer, AIRSPEED, NewReal(-AIRSPEEDFACTOR));
		}
		return ObjTrue;
	}
    }
    return ObjFalse;
}

Bool AddObjToSpace(object, space, corral, loc, visType)
ObjPtr object;
ObjPtr space;
ObjPtr corral;
ObjPtr loc;
ObjPtr visType;
/*Adds object or a representation of it to space.
  If the class of object is
    iconClass    Finds a preferred visualization for REPOBJ and adds it
    visClass     Adds it directly
    fileClass	 Whines
    otherwise    Finds a preferred visualization and adds it

  loc is a location to put the new icon, or NULLOBJ
  visType is the preferred visualization, or NULLOBJ to get the preferred
*/
{
    ObjPtr repObj, icon, defaultIcon;
    ObjPtr name;
    ObjPtr contents;
    ObjPtr clock;
    FuncTyp AddControls;
    FuncTyp method;
    ObjPtr parents;

    /*If object is a template, make a new copy*/
    if (GetPredicate(object, TEMPLATEP))
    {
	FuncTyp method;

	method = GetMethodSurely("AddObjToSpace", object, CLONE);
	if (method)
	{
	    object = (*method)(object);
	}
	SetVar(object, TEMPLATEP, ObjFalse);
	SetVar(object, SELECTED, ObjFalse);
    }

    if (IsIcon(object))
    {
	/*It's an icon; got to find a REPOBJ*/
	object = GetVar(object, REPOBJ);
	if (!object)
	{
	    return false;
	}
    }
    if (IsFile(object))
    {
	/*It's a file.  Whine*/
	WarnUser(CW_CANNOTVISFILE);
	return false;
    }
    if (IsVisObj(object) && visType)
    {
	/*Go down to dataset level if visObject*/
	repObj = GetVar(object, MAINDATASET);
	if (!repObj)
	{
	    repObj = GetVar(object, REPOBJ);
	}
	object = repObj;
    }
    if (!IsVisObj(object))
    {
	/*It's not a visualization yet.  Gotta find one*/

	/*Always apply some filters*/
	repObj = MainFilters(object);

	object = NewVis(repObj, visType);
	if (!object)
	{
	    WarnUser(CW_CANNOTVISERROR);
	    return false;
	}
	if (OptionDown())
	{
	    SetVar(object, HIDDEN, ObjTrue);
	}
    }
    else
    {
	repObj = GetVar(object, MAINDATASET);
	if (!repObj)
	{
	    repObj = GetVar(object, REPOBJ);
	}
    }

    contents = GetVar(space, CONTENTS);
    PrefixList(contents, object);
    parents = GetListVar("AddObjToSpace", object, PARENTS);
    if (parents)
    {
	PrefixList(parents, space);
    }
    else
    {
	SetVar(object, PARENT, space);
    }
    ImInvalid(object);
    SetVar(object, SPACE, space);

    /*Make an icon that represents the field and put it in the corral*/
    icon = NewVisIcon(object);
    if (icon)
    {
	SetVar(icon, ICONLOC, loc);	
	SetVar(icon, SPACE, space);
	if (OptionDown())
	{
	    SetVar(object, HIDDEN, ObjTrue);
	}
	DropIconInCorral(corral, icon);
    }

    method = GetMethod(object, DROPPEDINSPACE);
    if (method)
    {
	(*method)(object, space);
    }

    /*Reinitialize the clock in the space*/
    clock = GetVar(space, CLOCK);
    if (clock)
    {
	ReinitController(clock);
    }

    return true;
}

Bool DeleteControllerFromSpace(controller, space, corral)
ObjPtr controller, space, corral;
/*Deletes a controller from a space and its icon from the corral*/
{
    ObjPtr spaces;
    ObjPtr contents;
    ThingListPtr list;

    spaces = GetListVar("DeleteControllerFromSpace", controller, SPACES);
    if (!spaces) return false;

    /*Remove the space reference from the controller*/
    if (0 == DeleteFromList(spaces, space)) return false;
    
    /*Remove the controller's icon from the corral*/
    contents = GetListVar("DeleteControllerFromSpace", corral, CONTENTS);
    list = LISTOF(contents);
    while (list)
    {
	ObjPtr foundController;
	foundController = GetVar(list -> thing, REPOBJ);
	if (foundController == controller)
	{
	    DeleteFromList(contents, list -> thing);
	    ImInvalid(corral);
	    break;
	}
	list = list -> next;
    }

    return true;
}

static ObjPtr BindClockToSpace(clock, space)
ObjPtr clock, space;
/*Makes space know about clock*/
{
    SetVar(space, CLOCK, clock);
    return ObjTrue;
}

static ObjPtr BindObserverToSpace(observer, space)
ObjPtr observer, space;
/*Makes space know about observer*/
{
    SetVar(space, OBSERVER, observer);
    return ObjTrue;
}

static ObjPtr BindRendererToSpace(renderer, space)
ObjPtr renderer, space;
/*Makes space know about renderer*/
{
    SetVar(space, RENDERER, renderer);
    return ObjTrue;
}

Bool AddControllerToSpace(controller, space, corral, loc)
ObjPtr controller;
ObjPtr space;
ObjPtr corral;
ObjPtr loc;
/*Adds a space controller to space at loc and puts its icon in corral.
*/
{
    ObjPtr repObj, icon, iconY;
    ObjPtr contents, spaces;
    ThingListPtr list;
    ObjPtr controllerClass;
    ObjPtr defaultIcon;
    ObjPtr name;
    FuncTyp method;

    if (GetPredicate(controller, ONEONLY) &&
	(controllerClass = GetVar(controller, CLASSID)) &&
	IsInt(controllerClass))
    {
	int cc;

	/*First see if there is already a controller there*/
	cc = GetInt(controllerClass);
	contents = GetListVar("AddControllerToSpace", corral, CONTENTS);
	if (!contents) return false;
	list = LISTOF(contents);
	while (list)
	{
	    ObjPtr foundController;
	    foundController = GetVar(list -> thing, REPOBJ);
	    if (IsController(foundController) && IntVarEql(foundController, CLASSID, cc))
	    {
		if (foundController == controller)
		{
		    /*This is really the same controller.  Just move the icon*/
		    if (loc)
		    {
			SetVar(list -> thing, ICONLOC, loc);
			ImInvalid(list -> thing);
		    }
		    return true;
		}
		else
		{
		    /*It's a new controller.  Delete the old one and fall through*/
		    DeleteControllerFromSpace(foundController, space, corral);
		    break;
		}
	    }
	    list = list -> next;
	}
    }
    /*Make an icon that represents the new controller and put it in the corral*/
    name = GetStringVar("AddControllerToSpace", controller, NAME);
    defaultIcon = GetObjectVar("AddControllerToSpace", controller, DEFAULTICON);
    if (defaultIcon)
    {
	icon = NewObject(defaultIcon, 0);
	SetVar(icon, NAME, name ? name : NewString("Controller"));
	SetVar(icon, ICONGREYED, GetVar(controller, HIDDEN));
    }
    else
    {
	icon = NewIcon(0, 0, ICONQUESTION,
		   name ? GetString(name) : "Controller");
	SetVar(icon, HELPSTRING, 
	    NewString("This icon represents a controller.  For some reason, \
the default icon could not be found, so the icon appears as a question mark.  \
Please report this as a bug in SciAn."));
    }
    SetVar(icon, SPACE, space);
    SetVar(icon, REPOBJ, controller);
    method = GetMethod(controller, BINDTOSPACE);
    if (method)
    {
	(*method)(controller, space);
    }
    SetVar(icon, ICONLOC, loc);

    /*Make the controller know about this space*/
    spaces = GetListVar("AddControllerToSpace", controller, SPACES);
    if (!spaces) return false;
    PrefixList(spaces, space);

    ImInvalid(controller);
    DropIconInCorral(corral, icon);

    return true;
}

ObjPtr NewSpace(left, right, bottom, top)
int left, right, bottom, top;
/*Makes a new Space with bounds left, right, bottom, top*/
{
    ObjPtr retVal;

    retVal = NewObject(spaceClass, 0);
	
    Set2DIntBounds(retVal, left, right, bottom, top);
    SetVar(retVal, CONTENTS, NewList());

    return retVal;
}

#ifdef HAVE_PROTOTYPES
void PrintClock(char *s, char *f, real t)
#else
void PrintClock(s, f, t)
char *s;
char *f;
real t;
#endif
/*Prints t in seconds to a string s using format f*/
{
    while (*f)
    {
	if (*f == '%')
	{
	    /*Format string*/
	    ++f;
	    if (*f == '%')
	    {
		/*Just print out a %*/
		*s++ = *f++;
	    }
	    else
	    {
		char *temp;	/*Pointer into tempStr to assemble*/
		real n;		/*Number to print*/
		long i;		/*Temporary integer*/
		/*It's a real format.  Start assembling*/

		temp = tempStr;
		*temp++ = '%';

		/*Skip over flag(s)*/
		while (*f == '-' || *f == '+' || *f == ' ' || *f == '#')
		{
		    *temp++ = *f++;
		}

		/*Skip over first number*/
		while (*f >= '0' && *f <= '9')
		{
		    *temp++ = *f++;
		}

		/*Skip over second number*/
		if (*f == '.')
		{
		    *temp++ = *f++;
		    while (*f >= '0' && *f <= '9')
		    {
			*temp++ = *f++;
		    }
		}

		/*Now see what the format is*/
		switch (*f)
		{
		    case 'h':
			/*Hours, [1,12]*/
			n = floor(t / 3600.0);
			i = n / 12.0;
			n = n - i * 12;
			if (n < 1.00) n += 12.00;
			break;
		    case 'H':
			/*Unrestricted hours*/
			n = t / 3600.0;
			break;
		    case 'i':
			/*Hours, [0,24)*/
			n = floor(t / 3600.0);
			i = n / 24.0;
			n = n - i * 24;
			break;
		    case 'a':
			/*am or pm*/
			n = t / 3600.0;
			i = n;
			i = i % 24;
			sprintf(s, "%s", i >= 12 ? "pm" : "am");
			++f;
			goto dontsprintf;
			break;
		    case 'A':
			/*AM or PM*/
			n = t / 3600.0;
			i = n / 24.0;
			i = i % 24;
			sprintf(s, "%s", i >= 12 ? "PM" : "AM");
			++f;
			goto dontsprintf;
			break;
		    case 'M':
			/*Unrestricted minutes*/
			n = t / 60.0;
			break;
		    case 'm':
			/*Minutes, 0 to 59*/
			n = floor(t / 60.0);
			i = n / 60.0;
			n = n - i * 60;
			break;
		    case 's':
			/*Seconds, 0 to 59*/
			i = t / 60.0;
			n = t - i * 60;
			n = floor(n);
			break;
		    case 'S':
			/*Unrestricted seconds*/
		    case 't':
		    case 'T':
			/*Just a timestep*/
			n = t;
			break;
		    case 'e':
		    case 'E':
		    case 'f':
		    case 'F':
		    case 'g':
		    case 'G':
			*temp++ = *f++;
			*temp = 0;
			sprintf(s, tempStr, t);
			goto dontsprintf;
			break;
		    case 'x':
		    case 'X':
		    case 'd':
		    case 'D':
			*temp++ = *f++;
			*temp = 0;
			sprintf(s, tempStr, (int) t);
			goto dontsprintf;
			break;
		    default:
			strcpy(s, "Bad format: ");
			while (*s) ++s;
			*s++ = *f++;
			*s = 0;
			goto dontsprintf;
		}
		++f;

		/*Ready to print*/
		*temp++ = 'f';
		*temp = 0;
		sprintf(s, tempStr, n);
dontsprintf:
		while (*s)
		{
		    ++s;
		}
	    }
	}
	else
	{
	    *s++ = *f++;
	}
    }
    *s = 0;
}

void ChangeClockForDataset(window)
WinInfoPtr window;
/*Changes a clock in window for globalDataset*/
{
    ObjPtr space, clock;
    ObjPtr timeBounds;
    real oldtb[2], tb[2];
    ObjPtr contents;
    ObjPtr repObj;
    ThingListPtr runner;

    space = FindSpace(window);
    if (!space)
    {
	ReportError("ChangeClockForDataset", "No space in window");
	return;
    }

    clock = GetObjectVar("ChangeClockForDataset", space, CLOCK);
    if (!clock)
    {
	return;
    }

    timeBounds = GetVar(clock, TIMEBOUNDS);
    if (timeBounds)
    {
	Array2CArray(oldtb, timeBounds);
    }
    else
    {
	oldtb[0] = oldtb[1] = missingData;
    }

    contents = GetListVar("ChangeClockForDataset", space, CONTENTS);
    if (!contents)
    {
	return;
    }

    tb[0] = tb[1] = missingData;
    runner = LISTOF(contents);
    while (runner)
    {
	repObj = GetVar(runner -> thing, MAINDATASET);
	if (!repObj) repObj = GetObjectVar("ChangeClockForDataset", runner -> thing, REPOBJ);
	if (repObj)
	{
	    ObjPtr objTime;
	    real otb[2];
	    MakeVar(repObj, TIMEBOUNDS);
	    objTime = GetVar(repObj, TIMEBOUNDS);
	    if (objTime)
	    {
		Array2CArray(otb, objTime);
		if (tb[0] == missingData)
		{
		    tb[0] = otb[0];
		}
		else
		{
		    tb[0] = MIN(tb[0], otb[0]);
		}

		if (tb[1] == missingData)
		{
		    tb[1] = otb[1];
		}
		else
		{
		    tb[1] = MAX(tb[1], otb[1]);
		}
	    }
	}
	runner = runner -> next;
    }

    if (tb[1] != oldtb[1] || tb[0] != oldtb[0])
    {
	if (tb[1] != missingData && tb[0] != missingData)
	{
	    timeBounds = NewRealArray(1, 2L);
	    CArray2Array(timeBounds, tb);
	    SetVar(clock, TIMEBOUNDS, timeBounds);
	    ImInvalid(clock);
	}
	else
	{
	    SetVar(clock, TIMEBOUNDS, NULLOBJ);
	}
    }
}

ObjPtr MakeClockTimeBounds(clock)
ObjPtr clock;
/*Makes a clock's time bounds for the first time*/
{
    ForAllVisWindows(ChangeClockForDataset);
    SetVar(clock, TIMEBOUNDS, GetVar(clock, TIMEBOUNDS));
    return ObjTrue;
}

static ObjPtr MakeSpaceFilterType(space)
ObjPtr space;
/*Makes a space's filter type*/
{
    ObjPtr renderer;

    renderer = GetObjectVar("MakeSpaceFilterType", space, RENDERER);
    if (!renderer)
    {
	return ObjFalse;
    }
    SetVar(space, FILTERTYPE, GetVar(renderer, FILTERTYPE));
    return ObjTrue;
}

static ObjPtr MakeSpaceRenderType(space)
ObjPtr space;
/*Makes a space's RENDERTYPE.  Returns ObjTrue if it had an effect,
  ObjFalse otherwise.*/
{
    ObjPtr renderer;
    ObjPtr spaceRenderType, renderType;

    renderer = GetObjectVar("MakeSpaceRenderType", space, RENDERER);
    if (!renderer)
    {
	return ObjFalse;
    }

    spaceRenderType = GetVar(space, RENDERTYPE);
    renderType = GetVar(renderer, RENDERTYPE);

    if (!Equal(spaceRenderType, renderType))
    {
 	WinInfoPtr window;
	window = (WinInfoPtr) GetWindowVar("MakeSpaceRenderType", space, OWNERWINDOW);
	if (!window) return ObjTrue;

	    switch(GetInt(renderType))
	    {
		case RT_RGB_HARDWARE:
		    DeferMessage((ObjPtr) window, SETTORGBMODE);
		    break;
		case RT_CMAP_HARDWARE:
		    DeferMessage((ObjPtr) window, SETTOCMAPMODE);
		    break;
	    }
    }
    SetVar(space, RENDERTYPE, renderType);
    return ObjTrue;
}

void ReinitController(controller)
ObjPtr controller;
/*Reinitializes a controller by sending it a REINIT message and then resolving
  it*/
{
    FuncTyp method;
    method = GetMethod(controller, REINIT);
    if (method)
    {
	(*method)(controller);
    }
}

int pdspSerialNum = 0;

static ObjPtr DeletePaletteDisplay(display)
ObjPtr display;
/*Deletes display*/
{
    return ObjTrue;
}

void AddPaletteToSpacePanel(palette, panel, space, x, y)
ObjPtr palette, panel, space;
int x, y;
/*Adds palette to panel.*/
{
    ObjPtr paletteDisplay;

    sprintf(tempStr, "Palette Display %d", ++pdspSerialNum); 
    paletteDisplay = NewPaletteDisplay(x - DSPPALETTEWIDTH / 2, x + DSPPALETTEWIDTH / 2, 
			y - DSPPALETTEHEIGHT / 2, y + DSPPALETTEHEIGHT / 2,
			tempStr, palette);
    PrefixList(GetVar(panel, CONTENTS), paletteDisplay);
    SetVar(paletteDisplay, PARENT, panel);
    SetMethod(paletteDisplay, DELETEICON, DeletePaletteDisplay);
    SetVar(paletteDisplay, STICKINESS, NewInt(FLOATINGLEFT + FLOATINGRIGHT + FLOATINGTOP + FLOATINGBOTTOM));
    SetTextFont(paletteDisplay, ANNOTFONT);
    SetTextSize(paletteDisplay, ANNOTFONTSIZE);
}
static ObjPtr DropInSpacePanel(panel, dropObj, x, y)
ObjPtr panel, dropObj;
int x, y;
/*Drops object in a panel beginning at x and y.  Returns
  true iff the drop really was in the panel.*/
{
    int left, right, bottom, top;

    Get2DIntBounds(panel, &left, &right, &bottom, &top);

    if (x >= left && x <= right && y >= bottom && y <= top)
    {
	/*Hey!  It really was a drop in the panel*/
	ObjPtr contents;
	ObjPtr firstIcon;
	ThingListPtr restIcons;
	ThingListPtr runner;
	ObjPtr repObj;
	ObjPtr space;
	ObjPtr iconLoc;
	real loc[2];
	int xDisp, yDisp;

	if (IsList(dropObj))
	{
	    restIcons = LISTOF(dropObj);
	    firstIcon = restIcons -> thing;
	    restIcons = restIcons -> next;
	}
	else if (IsIcon(dropObj))
	{
	    firstIcon = dropObj;
	    restIcons = 0;
	}
	else
	{
	    ReportError("DropInSpacePanel", "An object other than an icon was dropped");
	    return ObjFalse;
	}

	space = GetObjectVar("DropInSpacePanel", panel, SPACE);
	if (!space) return ObjFalse;

	iconLoc = GetFixedArrayVar("DropInSpacePanel", firstIcon, ICONLOC, 1, 2L);
	if (!iconLoc)
	{
	    return ObjFalse;
	}
	Array2CArray(loc, iconLoc);

    	/*Setup the new viewport*/
	StartPanel(left, right, bottom, top);


        x -= left;
        y -= bottom;

	xDisp = x - loc[0];
	yDisp = y - loc[1];

	/*Drop first icon*/
	repObj = GetVar(firstIcon, REPOBJ);
	if (IsClock(repObj))
	{
	}
	else if (IsPalette(repObj))
	{
	    AddPaletteToSpacePanel(repObj, panel, space, x, y);
	}

	/*Drop remaining icons*/
	runner = restIcons;
	while (runner)
	{
	    repObj = GetVar(runner -> thing, REPOBJ);
	    if (IsClock(repObj))
	    {
	    }
	    else if (IsPalette(repObj))
	    {
		iconLoc = GetFixedArrayVar("DropInSpacePanel", runner -> thing, ICONLOC, 1, 2L);
		if (!iconLoc) break;
		Array2CArray(loc, iconLoc);
		loc[0] += xDisp;
		loc[1] += yDisp;
		AddPaletteToSpacePanel(repObj, panel, space, (int) loc[0], (int) loc[1]);
	    }
	    runner = runner -> next;
	}

	StopPanel();
	return ObjTrue;
    }
    else
    {
	return ObjFalse;
    }
}

#ifdef HAVE_PROTOTYPES
void SetClock(ObjPtr clock, real time)
#else
void SetClock(clock, time)
ObjPtr clock;
real time;
#endif
/*Sets clock to time*/
{
    SetVar(clock, TIME, NewReal(time));
    ImInvalid(clock);
}

static ObjPtr MarkClockTime(clock, lateness)
ObjPtr clock;
double lateness;
/*Marks time in a clock after a delay of lateness*/
{
    ObjPtr curTime;
    ObjPtr var;
    ObjPtr timeBounds;
    real deltaTime, deltaTimePer;
    int deltaTimeUnits, deltaTimePerUnits;
    real time;
    real tb[2];
    ObjPtr name;
    ObjPtr whichDialog;
    WinInfoPtr dialog;
    ObjPtr spaces;
    ThingListPtr list;
    Bool wrap;
    ObjPtr runSpeed, runControl;

    DoNotDisturb(clock, MARKTIME);

    runControl = GetVar(clock, RUNCONTROL);

    runSpeed = GetVar(clock, RUNSPEED);
    if (!runSpeed)
    {
	return ObjTrue;
    }

    /*Get the delta time and delta time per units*/
    var = GetVar(clock, DTIMEUNITS);
    if (var)
    {
	deltaTimeUnits = GetInt(var);
    }
    else
    {
	deltaTimeUnits = 0;
    }

    var = GetVar(clock, DTIMEPERUNITS);
    if (var)
    {
	deltaTimePerUnits = GetInt(var);
    }
    else
    {
	deltaTimePerUnits = 0;
    }

    MakeVar(clock, DTIME);
    var = GetVar(clock, DTIME);
    if (!var || !IsReal(var))
    {
	/*Obviously don't want to do anything*/
	if (runControl)
	{
	    InhibitLogging(true);
	    SetValue(runControl, NewInt(RC_STOP));
	    InhibitLogging(false);
	}
	DoNotDisturb(clock, MARKTIME);
	return ObjTrue;
    }
    deltaTime = GetReal(var);

    var = GetVar(clock, DTIMEPER);
    if (var)
    {
	deltaTime /= GetReal(var);
    }
    if (deltaTimePerUnits)
    {
	deltaTime = deltaTime * fps;
    }

    wrap = GetPredicate(clock, CYCLECLOCK);

    if (deltaTime == 0.0)
    {
	return ObjTrue;
    }

    switch (GetInt(runSpeed))
    {
	case RC_STOP:
	    return ObjTrue;
	case RC_FORWARD:
	    break;
	case RC_REVERSE:
	    deltaTime = -deltaTime;
	    break;
	case RC_FAST_FORWARD:
	    deltaTime = 3.0 * deltaTime;
	    break;
	case RC_FAST_REVERSE:
	    deltaTime = -3.0 * deltaTime;
	    break;
    }


    if (lateness <= 0.0)
    {
	return ObjTrue;
    }

    MakeVar(clock, TIME);
    curTime = GetVar(clock, TIME);
    if (curTime)
    {
	time = GetReal(curTime);
    }
    else
    {
	time = 0.0;
    }

    /*Increment the time by the elapsed time*/
    if (deltaTimeUnits)
    {
	ObjPtr timeSteps;
	MakeVar(clock, TIMESTEPS);
	timeSteps = GetVar(clock, TIMESTEPS);
	if (timeSteps)
	{
	    real index, newTime;
	    index = SearchFuzzyReal(timeSteps, time);

	    index += deltaTime * lateness;

	    newTime = FuzzyRealIndex(timeSteps, index);

	    if (newTime != time)
	    {
		time = newTime;
	    }
	    else
	    {
		if (runControl)
		{
		    InhibitLogging(true);
		    SetValue(runControl, NewInt(RC_STOP));
		    InhibitLogging(false);
		}
	    }
	}
    }
    else
    {
	time += deltaTime * lateness;
    }

    /*Check against time bounds*/
    MakeVar(clock, TIMEBOUNDS);
    timeBounds = GetVar(clock, TIMEBOUNDS);
    if (timeBounds && IsRealArray(timeBounds) && RANK(timeBounds) == 1 &&
	DIMS(timeBounds)[0] == 2)
    {
	Array2CArray(tb, timeBounds);
    }
    else
    {
	tb[0] = 0.0;
	tb[1] = 1.0;
    }
    if (deltaTime > 0.0 && time > tb[1])
    {
	if (wrap)
	{
	    time = tb[0];
	    /*Another wakeup call*/
	    WakeMe(clock, MARKTIME, Clock() + 0.001);
	}
	else
	{
	    time = tb[1];
	    if (runControl)
	    {
		InhibitLogging(true);
		SetValue(runControl, NewInt(RC_STOP));
		InhibitLogging(false);
	    }
	}
    }
    else if (deltaTime < 0.0 && time < tb[0])
    {
	if (wrap)
	{
	    time = tb[1];
	    /*Another wakeup call*/
	    WakeMe(clock, MARKTIME, Clock() + 0.001);
	}
	else
	{
	    time = tb[0];
	    if (runControl)
	    {
		InhibitLogging(true);
		SetValue(runControl, NewInt(RC_STOP));
		InhibitLogging(false);
	    }
	}
    }
    else
    {
	/*Another wakeup call*/
	WakeMe(clock, MARKTIME, Clock() + 0.001);
    }

    /*Now change the clock*/
    InhibitLogging(true);
    SetClock(clock, time);
    InhibitLogging(false);

    return ObjTrue;
}

#ifdef HAVE_PROTOTYPES
void GetSliderRange(ObjPtr slider, real *loValue, real *hiValue)
#else
void GetSliderRange(slider, loValue, hiValue)
ObjPtr slider;
real *loValue, *hiValue;
#endif
/*Returns the range of slider into loValue and hiValue*/
{
    ObjPtr var;
    var = GetRealVar("GetSliderRange", slider, LOVALUE);
    if (var) *loValue = GetReal(var);
    var = GetRealVar("GetSliderRange", slider, HIVALUE);
    if (var) *hiValue = GetReal(var);
}



static ObjPtr ChangeRunControl(radioGroup)
ObjPtr radioGroup;
/*Changes the run control of a clock according to a newly pressed button*/
{
    ObjPtr clock;
    real loValue, hiValue;
    ObjPtr var;
    int value;

    var = GetValue(radioGroup);
    if (var) value = GetInt(var); else value = RC_STOP;
    
    clock = GetObjectVar("ChangeClockSpeed", radioGroup, REPOBJ);
    if (!clock) return ObjFalse;

    DoNotDisturb(clock, MARKTIME);

    SetVar(clock, RUNSPEED, NewInt(value));

    WakeMe(clock, MARKTIME, Clock() + 0.001);

    return ObjTrue;
}

static ObjPtr MakeClockTimeSteps(clock)
ObjPtr clock;
/*Makes a clock's TIMESTEPS variable*/
{
    long k;
    ObjPtr datasets;
    ObjPtr totalTimeSteps = NULLOBJ;

    MakeVar(clock, DATASETS);
    datasets = GetVar(clock, DATASETS);
    if (datasets)
    {
	for (k = 0; k < DIMS(datasets)[0]; ++k)
	{
	    ObjPtr timeSteps;
	    ObjPtr element;

	    element = GetObjectElement(datasets, &k);
	    MakeVar(element, TIMESTEPS);
	    timeSteps = GetVar(element, TIMESTEPS);
	    if (timeSteps)
	    {
		if (!totalTimeSteps)
		{
		    totalTimeSteps = timeSteps;
		}
		else
		{
		    totalTimeSteps = MergeRealArrays(totalTimeSteps, timeSteps);
		}
	    }
	}
    }
    totalTimeSteps = Uniq(totalTimeSteps);
    SetVar(clock, TIMESTEPS, totalTimeSteps);
    return ObjTrue;
}

static ObjPtr MakeClockDTime(clock)
ObjPtr clock;
/*Method to make a clock's DTIME*/
{
    real dt;
    ObjPtr datasets;
    Bool stepSet = false;
    real minTimeStep;
    long nTimeSteps = 0;
    long k;

		/*Calculate minimum time step*/
		MakeVar(clock, DATASETS);
		datasets = GetVar(clock, DATASETS);
		if (datasets)
		{
		    ObjPtr element;

		    for (k = 0; k < DIMS(datasets)[0]; ++k)
		    {
		        ObjPtr timeSteps;
			element = GetObjectElement(datasets, &k);

		        MakeVar(element, TIMESTEPS);
		        timeSteps = GetVar(element, TIMESTEPS);
		        if (timeSteps)
			{
			    ObjPtr deltas;
			    deltas = SortArray(Uniq(RealArrayDeltas(timeSteps)));

			    /*Set the step only if there is one*/
			    nTimeSteps = DIMS(timeSteps)[0];
			    if (DIMS(timeSteps)[0] > 1)
			    {
				if (stepSet)
				{
				    minTimeStep = MIN(minTimeStep,
					  *((real *) ELEMENTS(deltas)));
				}
				else
				{
				    minTimeStep = *((real *) ELEMENTS(deltas));
				    stepSet = true;
				}
			    }
			}
		    }
		}
		/*Estimate dt*/
		if (nTimeSteps > 1)
		{
		    k = nTimeSteps / 20.0;
		    if (k < 1) k = 1;
		    else if (k > 10) k = 10;
		    dt = minTimeStep * (real) k;
		}
		else
		{
		    dt = 1.0;
		}
		SetVar(clock, DTIME, NewReal(dt));

}

static ObjPtr ShowClockControls(clock,  windowName)
ObjPtr clock;
ObjPtr windowName;
/*Makes a new clock window to control clock.  Ignores ownerWindow and windowName*/
{
    WinInfoPtr clockWindow;
    ObjPtr name;
    ObjPtr var;
    ObjPtr panel;
    ObjPtr contents;
    ObjPtr button, checkBox;
    int left;
    ObjPtr format;
    WinInfoPtr dialogExists;
    ObjPtr whichDialog;

    if (!clock) return NULLOBJ;

    name = GetVar(clock, NAME);

    whichDialog = NewString("Clock");
    dialogExists = DialogExists((WinInfoPtr) clock, whichDialog);
    if (name)
    {
	strncpy(tempStr, GetString(name), TEMPSTRSIZE);
	tempStr[TEMPSTRSIZE] = 0;
    }
    else
    {
	strcpy(tempStr, "Clock");
    }
    clockWindow = GetDialog((WinInfoPtr) clock, whichDialog, tempStr, 
	CLWINWIDTH, CLWINHEIGHT, CLWINWIDTH, CLWINHEIGHT, WINUI + WINFIXEDSIZE);

    if (!dialogExists)
    {
	int left, right, bottom, top;

	SetVar((ObjPtr) clockWindow, REPOBJ, clock);

	/*Put in a help string*/
	SetVar((ObjPtr) clockWindow, HELPSTRING,
	    NewString("This window shows controls for a clock.  Using these \
controls, you can change the time displayed in all the spaces a clock controls \
or set time to advance forward or backward at a certain rate."));

	/*Add in a panel*/
	panel = NewPanel(greyPanelClass, 0, CLWINWIDTH, 0, CLWINHEIGHT);
	if (!panel)
	{
	    return ObjFalse;
	}
	contents = GetVar((ObjPtr) clockWindow, CONTENTS);
	PrefixList(contents, panel);
	SetVar(panel, PARENT, (ObjPtr) clockWindow);

	contents = GetListVar("ShowClockControls", panel, CONTENTS);
	if (contents)
	{
	    ObjPtr button, checkBox, radioGroup, control, titleBox;
	    ObjPtr timeBounds;
	    ObjPtr time;
	    ObjPtr textBox;
	    char readoutText[40];
	    ObjPtr genericHelp;
		
	    real dt, mdt;
	    int left, right, bottom, top;

	    left = MAJORBORDER;
	    right = CLWINWIDTH - MAJORBORDER;
	    top = CLWINHEIGHT - MAJORBORDER;

	    MakeVar(clock, TIME);
	    time = GetVar(clock, TIME);

	    genericHelp = NewString("This controls the rate at which the clock \
advances through time.  When the forward play button is pushed, the clock will \
begin to advance forward the number of seconds or time steps given by the first \
text box for every number of seconds or frames given by the second time box.\n\
\n\
The easiest way to understand this is to read the text boxes and check boxes as \
a complete sentence.  Examples:\n\n\
\"Advance 0.25 second(s) every 1 second(s).\"\n\
This causes the clock to step forward at a rate of one-quarter second for every \
second that passes in the real world.  In other words, the clock will step at one-quarter \
real time.  When recording on videodisc, this will result in a one-quarter real time \
display when the videodisc is played back.\n\
\n\
\"Advance 5 time step(s) every 1 second(s).\"\n\
This causes the clock to step forward through five timesteps of the data for every \
second.  If the timesteps are unevenly spaced, the clock will speed up and slow down \
as neccessary to ensure that 5 timesteps are displayed every second.\n\
\n\
\"Advance 0.3 second(s) every 1 frame(s).\"\n\
This causes the clock to step forward three seconds for every frames.  A frame is \
the smallest unit of time that can be displayed given the limitations of the graphics \
device.  Videodiscs have a fixed frame rate of 30 frames per second (or 25 for PAL systems).  \
The interactive display has no fixed frame rate; it varies with how much time it takes to produce \
a picture.  \n\
\n\
To see one time step after another as fast as they can be displayed, set the controls to \
\"Advance 1 time step(s) every 1 frame(s).\"");

	    /*Add a time control*/
	    control = NewTimeControl(left, right, top - CLTCHEIGHT, top, "Time Control");
	    PrefixList(contents, control);
	    SetVar(control, PARENT, panel);
	    AssocDirectControlWithVar(control, clock, TIME);

	    ReinitController(clock);

	    top -= CLTCHEIGHT + MINORBORDER;

	    /*Add a title box*/
	    left = MINORBORDER;
	    bottom = MINORBORDER;
	    right = CLWINWIDTH - MINORBORDER;
	    titleBox = NewTitleBox(left, right, bottom, top, "Animation");
	    PrefixList(contents, titleBox);
	    SetVar(titleBox, PARENT, panel);

	    top -= TITLEBOXTOP + MINORBORDER;
	    left += MINORBORDER;
	    right -= MINORBORDER;

	    MakeVar(clock, DTIME);
	    var = GetVar(clock, DTIME);
	    if (var)
	    {
		dt = GetReal(var);
	    }
	    else
	    {
		SetVar(clock, DTIME, NewReal(0.0));
		printf("Shouldn't have to set clock's dtime\n");
		dt = 0.0;
	    }

	    bottom = top - EDITBOXHEIGHT;
	    /*Create the Advance text box*/
	    textBox = NewTextBox(left, left + (right - left) / 4,
				 bottom + (EDITBOXHEIGHT - TEXTBOXHEIGHT) / 2 + EDITBOXDOWN,
				 top - (EDITBOXHEIGHT - TEXTBOXHEIGHT) / 2 + EDITBOXDOWN,
				 PLAIN, "Advance legend", "Advance");
	    SetVar(textBox, PARENT, panel);
	    PrefixList(contents, textBox);
	    SetTextAlign(textBox, CENTERALIGN);

	    /*Create the dTime editable text box*/
	    PrintNumber(readoutText, dt);
	    textBox = NewTextBox(left + (right - left) / 4, left + 2 * (right - left) / 4,
				 bottom, top,
				 EDITABLE + WITH_PIT + ONE_LINE,
				 "Delta Time Box", readoutText);
	    SetVar(textBox, PARENT, panel);
	    PrefixList(contents, textBox);
	    AssocTextRealControlWithVar(textBox, clock, DTIME, 0.0, plusInf, 0);
	    SetVar(textBox, HELPSTRING, genericHelp);

	    /*Create the seconds and timesteps box*/
	    radioGroup = NewRadioButtonGroup("Delta Time Units");
	    PrefixList(contents, radioGroup);
	    SetVar(radioGroup, PARENT, panel);

	    button = NewRadioButton(left + (right - left) / 4, left + 2 * (right - left) / 4,
				bottom - CHECKBOXSPACING - CHECKBOXHEIGHT, bottom - CHECKBOXSPACING, "second(s)");
	    AddRadioButton(radioGroup, button);
	    button = NewRadioButton(left + (right - left) / 4, left + 2 * (right - left) / 4,
				bottom - 2 * CHECKBOXHEIGHT - 2 * CHECKBOXSPACING, bottom - CHECKBOXHEIGHT - 2 * CHECKBOXSPACING, "time step(s)");
	    AddRadioButton(radioGroup, button);
 
	    var = GetVar(clock, DTIMEUNITS);
	    if (!var) SetVar(clock, DTIMEUNITS, NewInt(0));
	    AssocDirectControlWithVar(radioGroup, clock, DTIMEUNITS);
	    SetVar(radioGroup, HELPSTRING, genericHelp);

	    /*Create the Every text box*/
	    textBox = NewTextBox(left + 2 * (right - left) / 4, left + 3 * (right - left) / 4,
				 bottom + (EDITBOXHEIGHT - TEXTBOXHEIGHT) / 2 + EDITBOXDOWN,
				 top - (EDITBOXHEIGHT - TEXTBOXHEIGHT) / 2 + EDITBOXDOWN,
				 PLAIN, "Every legend", "every");
	    SetVar(textBox, PARENT, panel);
	    PrefixList(contents, textBox);
	    SetTextAlign(textBox, CENTERALIGN);

	    /*Create the dTimePer editable text box*/
	    var = GetVar(clock, DTIMEPER);
	    if (!var)
	    {
		SetVar(clock, DTIMEPER, NewReal(1.0));
		printf("Shouldn't have to set dtimeper\n");
	    }
	    PrintNumber(readoutText, var ? GetReal(var) : 1.0);
	    textBox = NewTextBox(left + 3 * (right - left) / 4, right,
				 bottom, top,
				 EDITABLE + WITH_PIT + ONE_LINE,
				 "Delta Time Per Box", readoutText);
	    SetVar(textBox, PARENT, panel);
	    PrefixList(contents, textBox);
	    AssocTextRealControlWithVar(textBox, clock, DTIMEPER, 0.0, plusInf, TR_NE_BOTTOM);
	    SetVar(textBox, HELPSTRING, genericHelp);

	    /*Create the seconds and frames box*/
	    radioGroup = NewRadioButtonGroup("Delta Time Per");
	    PrefixList(contents, radioGroup);
	    SetVar(radioGroup, PARENT, panel);
	    SetVar(radioGroup, HELPSTRING, genericHelp);

	    button = NewRadioButton(left + 3 * (right - left) / 4, right,
				bottom - CHECKBOXSPACING - CHECKBOXHEIGHT, bottom - CHECKBOXSPACING, "second(s)");
	    AddRadioButton(radioGroup, button);
	    button = NewRadioButton(left + 3 * (right - left) / 4, right,
				bottom - 2 * CHECKBOXHEIGHT - 2 * CHECKBOXSPACING, bottom - CHECKBOXHEIGHT - 2 * CHECKBOXSPACING, "frame (s)");
	    AddRadioButton(radioGroup, button);
 
	    var = GetVar(clock, DTIMEPERUNITS);
	    if (!var) SetVar(clock, DTIMEPERUNITS, NewInt(0));
	    AssocDirectControlWithVar(radioGroup, clock, DTIMEPERUNITS);

	    top = bottom - MAJORBORDER - 2 * CHECKBOXHEIGHT - 2 * CHECKBOXSPACING;

	    /*Create the cycle time radio group*/
	    radioGroup = NewRadioButtonGroup("Cycle the clock");
	    SetVar(radioGroup, PARENT, panel);
	    PrefixList(contents, radioGroup);
	    AssocDirectControlWithVar(radioGroup, clock, CYCLECLOCK);
	    SetVar(radioGroup, HELPSTRING,
		NewString("These radio buttons control whether the clock, when it is running forward \
or backward, stops at the endpoints in time or cycles around to the other end.\n"));

	    /*And buttons*/
	    button = NewRadioButton(left, (right + left) / 2,
				top - CHECKBOXHEIGHT,
				top,
				"Stop at endpoints");
	    SetVar(button, HELPSTRING,
		NewString("Click on this button to have the clock automatically stop when it reaches an endpoint."));
	    AddRadioButton(radioGroup, button);

	    button = NewRadioButton((right + left) / 2, right,
				top - CHECKBOXHEIGHT,
				top,
				"Cycle at endpoints");
	    SetVar(button, HELPSTRING,
		NewString("Click on this button to have the clock automatically cycle around it reaches an endpoint."));
	    AddRadioButton(radioGroup, button);

	    top -= MAJORBORDER + CHECKBOXHEIGHT;

	    /*Create the icon buttons*/
	    radioGroup = NewRadioButtonGroup("Speed Control");
	    SetVar(radioGroup, PARENT, panel);
	    SetVar(radioGroup, REPOBJ, clock);
	    SetVar(radioGroup, HELPSTRING,
		NewString("These radio buttons control the speed of the clock.  \
They are tied to the Delta Time Per Second slider and provide easy access to some convenient values.  \
The most useful is Stop, the button in the center with the square icon."));
	    PrefixList(contents, radioGroup); 

	    /*Stop*/
	    button = NewIconButton((left + right) / 2 - ICONBUTTONSIZE / 2,
				   (left + right) / 2 + ICONBUTTONSIZE / 2,
				   top - ICONBUTTONSIZE, top,
				   ICONSTOP, UIGREEN, "Stop", BS_PLAIN);
	    SetVar(button, REPOBJ, clock);
	    SetVar(button, HELPSTRING, NewString("This button stops the animation."));
	    AddRadioButton(radioGroup, button);

	    /*Forward*/
	    button = NewIconButton((left + right) / 2 - ICONBUTTONSIZE / 2 + ICONBUTTONSIZE + MINORBORDER,
				   (left + right) / 2 + ICONBUTTONSIZE / 2 + ICONBUTTONSIZE + MINORBORDER,
				   top - ICONBUTTONSIZE, top,
				   ICONPLAY, UIGREEN, "Play", BS_PLAIN);
	    SetVar(button, REPOBJ, clock);
	    SetVar(button, HELPSTRING, NewString("This button sets time moving forward at the rate specified above."));
	    AddRadioButton(radioGroup, button);

	    /*Reverse*/
	    button = NewIconButton((left + right) / 2 - ICONBUTTONSIZE / 2 - ICONBUTTONSIZE - MINORBORDER,
				   (left + right) / 2 + ICONBUTTONSIZE / 2 - ICONBUTTONSIZE - MINORBORDER,
				   top - ICONBUTTONSIZE, top,
				   ICONREV, UIGREEN, "Reverse", BS_PLAIN);
	    SetVar(button, REPOBJ, clock);
	    SetVar(button, HELPSTRING, NewString("This button sets time moving backward at the rate specified above."));
	    AddRadioButton(radioGroup, button);

	    /*Fast Forward*/
	    button = NewIconButton((left + right) / 2 - ICONBUTTONSIZE / 2 + 2 * (ICONBUTTONSIZE + MINORBORDER),
				   (left + right) / 2 + ICONBUTTONSIZE / 2 + 2 * (ICONBUTTONSIZE + MINORBORDER),
				   top - ICONBUTTONSIZE, top,
				   ICONFF, UIGREEN, "Fast Forward", BS_PLAIN);
	    SetVar(button, REPOBJ, clock);
	    SetVar(button, HELPSTRING, NewString("This button sets time moving forward at three times the rate specified above."));
	    AddRadioButton(radioGroup, button);

	    /*Fast Reverse*/
	    button = NewIconButton((left + right) / 2 - ICONBUTTONSIZE / 2 - 2 * (ICONBUTTONSIZE + MINORBORDER),
				   (left + right) / 2 + ICONBUTTONSIZE / 2 - 2 * (ICONBUTTONSIZE + MINORBORDER),
				   top - ICONBUTTONSIZE, top,
				   ICONFR, UIGREEN, "Fast Reverse", BS_PLAIN);
	    SetVar(button, REPOBJ, clock);
	    SetVar(button, HELPSTRING, NewString("This button sets time moving backward at three times the rate specified above."));
	    AddRadioButton(radioGroup, button);

	    var = GetVar(clock, RUNSPEED);
	    if (var)
	    {
		SetValue(radioGroup, var);
	    }
	    else
	    {
		SetValue(radioGroup, NewInt(RC_STOP));
	    }

	    SetMethod(radioGroup, CHANGEDVALUE, ChangeRunControl);
	    SetVar(clock, RUNCONTROL, radioGroup);
	}
    }

    return (ObjPtr) clockWindow;
}

static ObjPtr MakePerspecControlAppearance(control)
ObjPtr control;
/*Makes a perspec control's appearance*/
{
    ObjPtr observer;
    FuncTyp method;
    ObjPtr var;
    int viewType;
    real perspecValue[4];

    /*Get the observer*/
    observer = GetObjectVar("MakePerspecControlAppearance", control, REPOBJ);
    if (!observer)
    {
	return ObjFalse;
    }

    var = GetVar(observer, FOCUSDIST);
    if (var)
    {
	perspecValue[0] = GetReal(var);
    }
    else
    {
	perspecValue[0] = INITEYEDIST;
    }
    var = GetVar(observer, VIEWFIELD);
    if (var)
    {
	perspecValue[1] = GetReal(var);
    }
    else
    {
	perspecValue[1] = INITAOV;
    }
    var = GetVar(observer, VIEWCLIP);
    if (var)
    {
	Array2CArray(&(perspecValue[2]), var);
    }
    else
    {
	perspecValue[2] = INITNEARCLIP;
	perspecValue[3] = INITFARCLIP;
    }
    if (perspecValue[2] < MINCLIP) perspecValue[2] = MINCLIP;
    if (perspecValue[3] < MINCLIP) perspecValue[3] = MINCLIP;

    var = NewRealArray(1, 4L);
    CArray2Array(var, perspecValue);
    method = GetMethod(control, CHANGEDVALUE);
    SetMethod(control, CHANGEDVALUE, (FuncTyp) 0);
    InhibitLogging(true);
    SetValue(control, var);
    InhibitLogging(false);
    SetMethod(control, CHANGEDVALUE, method);

    var = GetIntVar("ChangeViewType", observer, VIEWTYPE);
    if (!var)
    {
	return ObjFalse;
    }
    viewType = GetInt(var);

    if (viewType == VT_ORTHOGRAPHIC)
    {
	if (MakePerspecOrtho(control, true))
	{
	    InhibitLogging(true);
	    ChangedValue(control);
	    InhibitLogging(false);
	}
    }
    else
    {
	if (MakePerspecOrtho(control, false))
	{
	    InhibitLogging(true);
	    ChangedValue(control);
	    InhibitLogging(false);
	}
    }

    SetVar(control, APPEARANCE, ObjTrue);

    return ObjTrue;
}

static ObjPtr ChangePerspective(object)
ObjPtr object;
/*Change value for a perspective control*/
{
    ObjPtr val;
    ObjPtr repObj;
    real oldFocusDist;
    real newPerspec[4];

    repObj = GetObjectVar("ChangePerspective", object, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    val = GetRealVar("ChangePerspective", repObj, FOCUSDIST);
    if (!val)
    {
	return ObjFalse;
    }
    oldFocusDist = GetReal(val);

    val = GetFixedArrayVar("ChangePerspective", object, VALUE, 1, 4L);
    if (!val)
    {
	return ObjFalse;
    }
    Array2CArray(newPerspec, val);

    /*Change the observer's bits*/
    SetVar(repObj, VIEWFIELD, NewReal(newPerspec[1]));
    val = NewRealArray(1, 2L);
    CArray2Array(val, &(newPerspec[2]));
    SetVar(repObj, VIEWCLIP, val);

    /*See if field distance has changed*/
    if (oldFocusDist != newPerspec[0])
    {
	/*If so, change LOCATION accordingly*/
	real posn[3];
	real focus[3];
	real forward[3];
	ObjPtr var;

	GetFocusPoint(focus, repObj);
	GetObserverLocation(forward, repObj);
	forward[0] = focus[0] - forward[0];
	forward[1] = focus[1] - forward[1];
	forward[2] = focus[2] - forward[2];
	NORMALIZE(forward);

	posn[0] = focus[0] - forward[0] * newPerspec[0];
	posn[1] = focus[1] - forward[1] * newPerspec[0];
	posn[2] = focus[2] - forward[2] * newPerspec[0];
	SetVar(repObj, FOCUSDIST, NewReal(newPerspec[0]));
	var = NewRealArray(1, 3L);
	CArray2Array(var, posn);
	SetVar(repObj, LOCATION, var);
   }

   ImInvalid(repObj);
   return ObjTrue;
}

ObjPtr ChangeAirspeed(slider)
ObjPtr slider;
/*Changes the airspeed of an observer according to a slider*/
{
    ObjPtr controller;
    ObjPtr value;

    controller = GetObjectVar("ChangeAirspeed", slider, REPOBJ);
    if (!controller)
    {
	return ObjFalse;
    }

    value = GetRealVar("ChangeAirspeed", slider, VALUE);
    if (!value)
    {
	return ObjFalse;
    }

    SetVar(controller, AIRSPEED, value);
    ImInvalid(controller);

    return ObjTrue;
}

ObjPtr ChangeFlying(radio)
ObjPtr radio;
/*Changes whether an observer is flying based on radio*/
{
    ObjPtr repObj, var;
    Bool flying;
    repObj = GetObjectVar("ChangeFlying", radio, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }
    var = GetValue(radio);
    if (!var)
    {
	return ObjFalse;
    }
    flying = GetInt(var) ? true : false;
    if (flying)
    {
    	WakeMe(repObj, MARKTIME, Clock() + 0.001);
    }

    SetVar(repObj, FLYING, flying ? ObjTrue : ObjFalse);
    ImInvalid(repObj);
    return ObjTrue;
}

ObjPtr ResetPosition(button)
ObjPtr button;
/*Resets an observer's position*/
{
    ObjPtr observer, var;
    real focusDist;
    real forward[3];
    real eyePosn[3];
    int i;

    observer = GetObjectVar("ResetPosition", button, REPOBJ);
    if (!observer)
    {
	return ObjFalse;
    }

    focusDist = INITEYEDIST;
    var = GetRealVar("ResetPosition", observer, FOCUSDIST);
    if (var)
    {
	focusDist = GetReal(var);
    }

    GetForwardVector(forward, observer);

    /*Derive the eye position from 0,0,0 through eye distance*/
    
    eyePosn[0] = -forward[0] * focusDist;
    eyePosn[1] = -forward[1] * focusDist;
    eyePosn[2] = -forward[2] * focusDist;
    var = NewRealArray(1, 3L);
    CArray2Array(var, eyePosn);
    SetVar(observer, LOCATION, var);

    ImInvalid(observer);

    return ObjTrue;
}

ObjPtr ResetRotation(button)
ObjPtr button;
/*Resets an observer's rotation*/
{
    ObjPtr observer;
    real focusPoint[3];
    real eyePosn[3];
    ObjPtr var;
    real focusDist;
    Quaternion rotQuat;

    observer = GetObjectVar("ResetRotation", button, REPOBJ);
    if (!observer)
    {
	return ObjFalse;
    }

    focusDist = INITEYEDIST;
    var = GetRealVar("ResetRotation", observer, FOCUSDIST);
    if (var)
    {
	focusDist = GetReal(var);
    }

    GetFocusPoint(focusPoint, observer);

    rotQuat[0] = 1.0;
    rotQuat[1] = 0.0;
    rotQuat[2] = 0.0;
    rotQuat[3] = 0.0;
    var = NewRealArray(1, 4L);
    CArray2Array(var, rotQuat);
    SetVar(observer, ROTQUAT, var);

    eyePosn[0] = focusPoint[0];
    eyePosn[1] = focusPoint[1];
    eyePosn[2] = focusPoint[2] + focusDist;
    var = NewRealArray(1, 3L);
    CArray2Array(var, eyePosn);
    SetVar(observer, LOCATION, var);

    ImInvalid(observer);

    return ObjTrue;
}

static ObjPtr ChangeObserverLookat(control)
ObjPtr control;
/*Changes an observers's lookat point*/
{
    ObjPtr repObj, var;
    NameTyp whichVar;
    long whichIndex;
    long nIndices;
    long k;
    real *el1, *el2;
    real minMax[2];
    real value;
    char *s;
    real forward[3], location[3], up[3], side[3], lookat[3], focusDist;
    real newRot[3][3];
    Quaternion rotQuat;

    repObj = GetVarSurely("ChangeObserverLookat", control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    var = GetIntVar("ChangeObserverLookat", control, WHICHINDEX);
    if (!var)
    {
	return ObjFalse;
    }
    whichIndex = GetInt(var);

    var = GetRealVar("ChangeObserverLookat", repObj, FOCUSDIST);
    if (!var)
    {
	return ObjFalse;
    }
    focusDist = GetReal(var);

    GetForwardVector(forward, repObj);
    GetUpVector(up, repObj);
    GetObserverLocation(location, repObj);

    minMax[0] = minusInf;
    minMax[1] = plusInf;

    var = GetValue(control);
    if ((!var) || (!IsString(var)))
    {
	return ObjFalse;
    }

    s = GetString(var);

    if (ParseReal(&value, s))
    {
	if (value == missingData)
	{
	    {
		WarnUser(CW_MISSINGERROR);
		return ObjFalse;
	    }
	}
	else
	{
	    {
		if (value <= minMax[0])
		{
		    DeferMessage(control, RANGEALERT);
		    return ObjFalse;
		}
	    }

	    {
		if (value >= minMax[1])
		{
		    DeferMessage(control, RANGEALERT);
		    return ObjFalse;
		}
	    }
	}

	/*Dropped through, it must be OK*/
	if (GetPredicate(repObj, USESPACECOORDS))
	{
	    real *elements;
	    ObjPtr var;

	    /*Have to convert into observer coords*/

	    var = GetFixedArrayVar("MakeObserverLocationAppearance", repObj, SPACEORIGIN, 1, 3L);
	    if (var)
	    {
		elements = ELEMENTS(var);
		value -= elements[whichIndex];
	    }

	    var = GetFixedArrayVar("MakeObserverLocationAppearance", repObj, SPACESCALE, 1, 3L);
	    if (var)
	    {
		elements = ELEMENTS(var);
		value /= elements[whichIndex];
	    }
	}

    }
    else
    {
	WarnUser(CW_NUMBERERROR);
	return ObjFalse;
    }

    /*Now use the new value to make a new lookat*/
    lookat[0] = location[0] + forward[0] * focusDist;
    lookat[1] = location[1] + forward[1] * focusDist;
    lookat[2] = location[2] + forward[2] * focusDist;
    lookat[whichIndex] = value;

    forward[0] = lookat[0] - location[0];
    forward[1] = lookat[1] - location[1];
    forward[2] = lookat[2] - location[2];

    focusDist = sqrt(SQUARE(forward[0]) + SQUARE(forward[1]) + SQUARE(forward[2]));
    if (focusDist < 0.0)
    {
	WarnUser(CW_LOOKATERROR);
	return ObjFalse;
    }

    /*Make a new forward, up, and distance*/
    forward[0] /= focusDist;
    forward[1] /= focusDist;
    forward[2] /= focusDist;

    CROSS(forward, up, side);
    if (side[0] == 0.0 && side[1] == 0.0 && side[2] == 0.0)
    {
	side[0] = forward[1];
	side[1] = forward[2];
	side[2] = forward[0];
    }
    NORMALIZE(side);
    CROSS(side, forward, up);
    NORMALIZE(up);

    /*Make a matrix*/
    newRot[0][0] = side[0];
    newRot[1][0] = side[1];
    newRot[2][0] = side[2];

    newRot[0][1] = up[0];
    newRot[1][1] = up[1];
    newRot[2][1] = up[2];

    newRot[0][2] = -forward[0];
    newRot[1][2] = -forward[1];
    newRot[2][2] = -forward[2];

    MatrixToQuaternion(newRot, rotQuat);
    var = NewRealArray(1, 4L);
    CArray2Array(var, rotQuat);
    SetVar(repObj, ROTQUAT, var);

    SetVar(repObj, FOCUSDIST, NewReal(focusDist));

    ImInvalid(repObj);
    SetVar(control, APPEARANCE, ObjTrue);

    return ObjTrue;
}

static ObjPtr MakeObserverLookatAppearance(control)
ObjPtr control;
/*Gets an observer's location and makes appearance*/
{
    real *location, forward[3], focusDist;
    ObjPtr repObj;
    ObjPtr var;
    real value;
    int whichAxis;
    FuncTyp method;
    real observerCoord;

    repObj = GetVar(control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    MakeVar(repObj, LOCATION);
    var = GetFixedArrayVar("MakeObserverLookatAppearance", repObj, LOCATION, 1, 3L);
    if (!var)
    {
	return ObjFalse;
    }
    location = ELEMENTS(var);

    GetForwardVector(forward, repObj);

    MakeVar(repObj, FOCUSDIST);
    var = GetRealVar("MakeObserverLookatAppearance", repObj, FOCUSDIST);
    if (!var)
    {
	return ObjFalse;
    }
    focusDist = GetReal(var);

    var = GetIntVar("MakeObserverLookatAppearance", control, WHICHINDEX);
    if (var)
    {
	whichAxis = GetInt(var);
    }
    else
    {
	whichAxis = 0;
    }

    observerCoord = location[whichAxis] + forward[whichAxis] * focusDist;

    if (GetPredicate(repObj, USESPACECOORDS))
    {
	real *elements;

	/*Have to convert into space coords*/
	var = GetFixedArrayVar("MakeObserverLookatAppearance", repObj, SPACESCALE, 1, 3L);
	if (var)
	{
	    elements = ELEMENTS(var);
	    observerCoord *= elements[whichAxis];
	}

	var = GetFixedArrayVar("MakeObserverLookatAppearance", repObj, SPACEORIGIN, 1, 3L);
	if (var)
	{
	    elements = ELEMENTS(var);
	    observerCoord += elements[whichAxis];
	}
    }

    PrintNumber(tempStr, observerCoord);
    method = GetMethod(control, CHANGEDVALUE);
    SetMethod(control, CHANGEDVALUE, (FuncTyp) 0);
    InhibitLogging(true);
    SetValue(control, NewString(tempStr));
    InhibitLogging(false);
    SetMethod(control, CHANGEDVALUE, method);

    SetVar(control, APPEARANCE, ObjTrue);
    ImInvalid(control);
    return ObjTrue;
}

static ObjPtr ChangeObserverUp(control)
ObjPtr control;
/*Changes an observers's up vector */
{
}

static ObjPtr MakeObserverUpAppearance(control)
ObjPtr control;
/*Gets an observer's up and makes appearance*/
{
    real *elements;
    ObjPtr repObj;
    ObjPtr var;
    real value;
    int whichAxis;
    FuncTyp method;

    repObj = GetVar(control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    MakeVar(repObj, UPVECTOR);
    var = GetFixedArrayVar("MakeObserverUpAppearance", repObj, UPVECTOR, 1, 3L);
    if (!var)
    {
	return ObjFalse;
    }

    elements = ELEMENTS(var);

    var = GetIntVar("MakeObserverUpAppearance", control, WHICHINDEX);
    if (var)
    {
	whichAxis = GetInt(var);
    }
    else
    {
	whichAxis = 0;
    }

    PrintNumber(tempStr, elements[whichAxis]);
    method = GetMethod(control, CHANGEDVALUE);
    SetMethod(control, CHANGEDVALUE, (FuncTyp) 0);
    InhibitLogging(true);
    SetValue(control, NewString(tempStr));
    InhibitLogging(false);
    SetMethod(control, CHANGEDVALUE, method);

    SetVar(control, APPEARANCE, ObjTrue);
    ImInvalid(control);
    return ObjTrue;
}

static ObjPtr ChangeObserverQuaternion(control)
ObjPtr control;
/*Changes an observers's quaternion */
{
    ObjPtr repObj;
    ObjPtr var;
    Quaternion oldQuat, newQuat;
    real value;
    int whichAxis;
    int prevAxis, nextAxis;
    real sign;
    real magnitude, desMag, mult;
    char *s;

    repObj = GetVar(control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    MakeVar(repObj, ROTQUAT);
    var = GetFixedArrayVar("MakeObserverQuaternionAppearance", repObj, ROTQUAT, 1, 4L);
    if (!var)
    {
	return ObjFalse;
    }
    Array2CArray(oldQuat, var);

    var = GetIntVar("MakeObserverQuaternionAppearance", control, WHICHINDEX);
    if (var)
    {
	whichAxis = GetInt(var);
    }
    else
    {
	whichAxis = 0;
    }

    var = GetValue(control);
    if (!var)
    {
	return ObjFalse;
    }
    s = GetString(var);
    if (!ParseReal(&value, s))
    {
	return ObjFalse;
    }
    if (value <= minusInf || value >= plusInf)
    {
	WarnUser(CW_INFINITYERROR);
	return ObjFalse;
    }
    if (value == missingData)
    {
	WarnUser(CW_MISSINGERROR);
	return ObjFalse;
    }
    if (value > 1.0) value = 1.0;
    if (value < -1.0) value = -1.0;

    if (whichAxis == 0)
    {
	/*Just change amount of rotation, keep axis the same*/
	newQuat[0] = value;
	magnitude = sqrt(SQUARE(oldQuat[1]) + SQUARE(oldQuat[2]) + SQUARE(oldQuat[3]));
	if (magnitude > 0.0)
	{
	    /*Have to adjust magnitude*/
	    desMag = sqrt(1.0 - SQUARE(newQuat[0]));
	    mult = desMag / magnitude;

	    newQuat[1] = oldQuat[1] * mult;
	    newQuat[2] = oldQuat[2] * mult;
	    newQuat[3] = oldQuat[3] * mult;
	}
	else
	{
	    /*Pick an arbitrary axis*/
	    newQuat[1] = sqrt(1.0 - SQUARE(newQuat[0]));
	    newQuat[2] = 0.0;
	    newQuat[3] = 0.0;
	}
    }
    else
    {
	/*One of the vector components*/
	newQuat[0] = oldQuat[0];
	newQuat[1] = oldQuat[1];
	newQuat[2] = oldQuat[2];
	newQuat[3] = oldQuat[3];

	desMag = sqrt(1.0 - SQUARE(newQuat[0]));
	if (value > desMag) value = desMag;
	if (value < -desMag) value = -desMag;
	newQuat[whichAxis] = value;

	/*Try to fiddle just the next axis*/
	prevAxis = whichAxis - 1;
	if (prevAxis == 0) prevAxis = 3;
	nextAxis = whichAxis + 1;
	if (nextAxis == 4) nextAxis = 1;

	magnitude = sqrt(SQUARE(newQuat[whichAxis]) + SQUARE(newQuat[prevAxis]));
	if (magnitude <= desMag)
	{
	    /*It can be done!*/
	    sign = newQuat[nextAxis] >= 0.0 ? 1.0 : -1.0;

	    newQuat[nextAxis] = sqrt(SQUARE(desMag) - SQUARE(magnitude)) * sign;
	}
	else
	{
	    /*It can't be done.  Flatten to zero and make minimal adjustment
	      to prevAxis*/
	    newQuat[nextAxis] = 0.0;
	    sign = newQuat[prevAxis] >= 0.0 ? 1.0 : -1.0;
	    newQuat[prevAxis] = sqrt(SQUARE(desMag) - SQUARE(newQuat[whichAxis])) * sign;
	}
    }

    var = NewRealArray(1, 4L);
    CArray2Array(var, newQuat);
    SetVar(repObj, ROTQUAT, var);
    ImInvalid(repObj);

    return ObjTrue;
}

static ObjPtr MakeObserverQuaternionAppearance(control)
ObjPtr control;
/*Gets an observer's up and makes appearance*/
{
    real *elements;
    ObjPtr repObj;
    ObjPtr var;
    real value;
    int whichAxis;
    FuncTyp method;

    repObj = GetVar(control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    MakeVar(repObj, ROTQUAT);
    var = GetFixedArrayVar("MakeObserverQuaternionAppearance", repObj, ROTQUAT, 1, 4L);
    if (!var)
    {
	return ObjFalse;
    }

    elements = ELEMENTS(var);

    var = GetIntVar("MakeObserverQuaternionAppearance", control, WHICHINDEX);
    if (var)
    {
	whichAxis = GetInt(var);
    }
    else
    {
	whichAxis = 0;
    }

    PrintNumber(tempStr, elements[whichAxis]);
    method = GetMethod(control, CHANGEDVALUE);
    SetMethod(control, CHANGEDVALUE, (FuncTyp) 0);
    InhibitLogging(true);
    SetValue(control, NewString(tempStr));
    InhibitLogging(false);
    SetMethod(control, CHANGEDVALUE, method);

    SetVar(control, APPEARANCE, ObjTrue);
    ImInvalid(control);
    return ObjTrue;
}

static ObjPtr HideOnAutoAdjust(control)
ObjPtr control;
/*Hides a control based on the AUTOADJUST of its repobj*/
{
    ObjPtr repObj;

    repObj = GetVar(control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }
    ActivateTextBox(control, GetPredicate(repObj, AUTOADJUST) ? false : true);

    return ObjTrue;
}

static ObjPtr ChangeObserverLocation(control)
ObjPtr control;
/*Changes an observer's location value*/
{
    ObjPtr repObj, var;
    NameTyp whichVar;
    long whichIndex;
    long nIndices;
    long k;
    real *el1, *el2;
    real minMax[2];
    real value;
    char *s;

    repObj = GetVarSurely("ChangeObserverLocation", control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    whichVar = LOCATION;

    var = GetIntVar("ChangeObserverLocation", control, WHICHINDEX);
    if (!var)
    {
	return ObjFalse;
    }
    whichIndex = GetInt(var);

    minMax[0] = minusInf;
    minMax[1] = plusInf;

    var = GetValue(control);
    if ((!var) || (!IsString(var)))
    {
	return ObjFalse;
    }

    s = GetString(var);

    if (ParseReal(&value, s))
    {
	if (value == missingData)
	{
	    {
		WarnUser(CW_MISSINGERROR);
		return ObjFalse;
	    }
	}
	else
	{
	    {
		if (value <= minMax[0])
		{
		    DeferMessage(control, RANGEALERT);
		    return ObjFalse;
		}
	    }

	    {
		if (value >= minMax[1])
		{
		    DeferMessage(control, RANGEALERT);
		    return ObjFalse;
		}
	    }
	}

	/*Dropped through, it must be OK*/
	var = GetArrayVar("ChangeObserverLocation", repObj, whichVar);
	if (!var)
	{
	    return ObjFalse;
	}
	el1 = ELEMENTS(var);
	nIndices = DIMS(var)[0];
	var = NewRealArray(1, nIndices);
	el2 = ELEMENTS(var);
	for (k = 0; k < nIndices; ++k)
	{
	    el2[k] = el1[k];
	}
	if (GetPredicate(repObj, USESPACECOORDS))
	{
	    real *elements;
	    ObjPtr var;

	    /*Have to convert into observer coords*/

	    var = GetFixedArrayVar("MakeObserverLocationAppearance", repObj, SPACEORIGIN, 1, 3L);
	    if (var)
	    {
		elements = ELEMENTS(var);
		value -= elements[whichIndex];
	    }

	    var = GetFixedArrayVar("MakeObserverLocationAppearance", repObj, SPACESCALE, 1, 3L);
	    if (var)
	    {
		elements = ELEMENTS(var);
		value /= elements[whichIndex];
	    }
	}

	el2[whichIndex] = value;
	SetVar(repObj, whichVar, var);

	ImInvalid(repObj);
    }
    else
    {
	WarnUser(CW_NUMBERERROR);
	return ObjFalse;
    }

    SetVar(control, APPEARANCE, ObjTrue);

    return ObjTrue;
}

static ObjPtr MakeObserverLocationAppearance(control)
ObjPtr control;
/*Makes a text real control's appearance based on the variable it controls*/
{
    ObjPtr repObj, var, value;
    NameTyp whichVar;
    long whichIndex;
    FuncTyp method;
    real observerCoord;

    repObj = GetVarSurely("MakeObserverLocationAppearance", control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    whichVar = LOCATION;

    var = GetIntVar("MakeObserverLocationAppearance", control, WHICHINDEX);
    if (!var)
    {
	return ObjFalse;
    }
    whichIndex = GetInt(var);

    var = GetArrayVar("MakeObserverLocationAppearance", repObj, whichVar);
    if (var)
    {
	real *elements;
	elements = ELEMENTS(var);
	observerCoord = elements[whichIndex];
    }
    else
    {
	observerCoord = 0.0;
    }

    if (GetPredicate(repObj, USESPACECOORDS))
    {
	real *elements;

	/*Have to convert into space coords*/
	var = GetFixedArrayVar("MakeObserverLookatAppearance", repObj, SPACESCALE, 1, 3L);
	if (var)
	{
	    elements = ELEMENTS(var);
	    observerCoord *= elements[whichIndex];
	}

	var = GetFixedArrayVar("MakeObserverLocationAppearance", repObj, SPACEORIGIN, 1, 3L);
	if (var)
	{
	    elements = ELEMENTS(var);
	    observerCoord += elements[whichIndex];
	}
    }

    PrintNumber(tempStr, observerCoord);
    InhibitLogging(true);
    method = GetMethod(control, CHANGEDVALUE);
    SetMethod(control, CHANGEDVALUE, (FuncTyp) 0);
    SetValue(control, NewString(tempStr));
    SetMethod(control, CHANGEDVALUE, method);
    method = GetMethod(control, EXTRAAPPEARANCE);
    if (method)
    {
	(*method)(control);
    }
    ImInvalid(control);
    InhibitLogging(false);
    SetVar(control, APPEARANCE, ObjTrue);

    return ObjTrue;
}

static ObjPtr MakeRadarAppearance(control)
ObjPtr control;
{
    real vec[3], adjVec[3];
    real loc[3];
    Quaternion rotQuat, qInv;
    ObjPtr var;
    ObjPtr repObj;
    real val[2];
    real r;

    repObj = GetVar(control, REPOBJ);
    if (!repObj) return ObjFalse;

    MakeVar(repObj, ROTQUAT);
    var = GetVar(repObj, ROTQUAT);
    if (!var) return ObjFalse;
    Array2CArray(rotQuat, var);

    MakeVar(repObj, LOCATION);
    var = GetVar(repObj, LOCATION);
    if (!var) return ObjFalse;
    Array2CArray(loc, var);

    vec[0] = - loc[0];
    vec[1] = - loc[1];
    vec[2] = - loc[2];

    QInvert(rotQuat, qInv);
    QRot(vec, qInv, adjVec);

    if ((adjVec[0] == 0.0) &&
	(adjVec[1] == 0.0) &&
	(adjVec[2] == 0.0))
    {
	val[0] = val[1] = 0.0;
    }
    else
    {
	NORMALIZE(adjVec);

	if ((adjVec[0] == 0.0) && (adjVec[2] == 0.0))
	{
	    val[0] = 0.0;
	    val[1] = 0.0;
	}
	else
	{
	    val[0] = ratan2(adjVec[0], -adjVec[2]);
	    r = rsqrt(SQUARE(adjVec[0]) + SQUARE(adjVec[2]));
	    val[1] = ratan2(adjVec[1], r);
	}
    }
    var = NewRealArray(1, 2L);
    CArray2Array(var, val);
    SetVar(control, VALUE, var);

    SetVar(control, APPEARANCE, ObjTrue);

    ImInvalid(control);
    return ObjTrue;
}

static ObjPtr ChangeRadar(control)
ObjPtr control;
/*Changes a radar control*/
{
    ObjPtr repObj, value;
    real *elements;
    real vec[3], adjVec[3];
    real loc[3];
    Quaternion rotQuat, qInv, dRot, qT;
    real dAzimuth, dElevation;
    ObjPtr var;
    real val[2];
    real forward[3], up[3], side[3];
    real r;

    repObj = GetVar(control, REPOBJ);
    if (!repObj)
    {
	return ObjFalse;
    }

    /*Get existing value*/
    MakeVar(repObj, ROTQUAT);
    var = GetVar(repObj, ROTQUAT);
    if (!var) return ObjFalse;
    Array2CArray(rotQuat, var);

    MakeVar(repObj, LOCATION);
    var = GetVar(repObj, LOCATION);
    if (!var) return ObjFalse;
    Array2CArray(loc, var);

    vec[0] = - loc[0];
    vec[1] = - loc[1];
    vec[2] = - loc[2];

    QInvert(rotQuat, qInv);
    QRot(vec, qInv, adjVec);

    if ((adjVec[0] == 0.0) &&
	(adjVec[1] == 0.0) &&
	(adjVec[2] == 0.0))
    {
	val[0] = val[1] = 0.0;
    }
    else
    {
	NORMALIZE(adjVec);

	if ((adjVec[0] == 0.0) && (adjVec[2] == 0.0))
	{
	    val[0] = 0.0;
	    val[1] = 0.0;
	}
	else
	{
	    val[0] = ratan2(adjVec[0], -adjVec[2]);
	    r = rsqrt(SQUARE(adjVec[0]) + SQUARE(adjVec[2]));
	    val[1] = ratan2(adjVec[1], r);
	}
    }

    value = GetValue(control);
    if (!value)
    {
	return ObjFalse;
    }

    elements = ELEMENTS(value);

    /*Now determine change in azimuth and elevation*/
    dAzimuth = val[0] - elements[0];
    dElevation = val[1] - elements[1];

    GetAdjustedVectors(forward, up, repObj);
    CROSS(forward, up, side);

    RotToQuaternion(up, dAzimuth, dRot);
    QMult(rotQuat, dRot, qT);
    RotToQuaternion(side, -dElevation, dRot);
    QMult(qT, dRot, rotQuat);
    var = NewRealArray(1, 4L);
    CArray2Array(var, rotQuat);
    SetVar(repObj, ROTQUAT, var);
    ImInvalid(repObj);
    LogObserver(repObj);

    return ObjTrue;
}

static ObjPtr AddViewControls(observer, panelContents)
ObjPtr observer, panelContents;
/*Adds observer controls to a panelContents*/
{
    ObjPtr textBox, titleBox, radio, button, control;
    ObjPtr var;
    int l, r, b, t;

    /*View type chooser*/
    titleBox = TemplateTitleBox(ObserverTemplate, "View Type");
    SetVar(titleBox, PARENT, panelContents);
    PrefixList(panelContents, titleBox);

    radio = NewRadioButtonGroup("View Type Radio");
    PrefixList(panelContents, radio);
    SetVar(radio, PARENT, panelContents);
    SetVar(radio, REPOBJ, observer);
    SetVar(radio, HELPSTRING, NewString("This radio group controls \
the type of view given in all spaces controlled by this observer.  For more information \
about a given view type, use Help In Context on the button naming the view type.\n"));

    button = TemplateRadioButton(ObserverTemplate, "Perspective");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, the standard \
perspective view is used.  Objects closer to the observer appear larger, giving a \
realistic image.\n"));

    button = TemplateRadioButton(ObserverTemplate, "Orthographic");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, the \
orthograpic view is enabled.  Objects appear the same size no matter what the \
distance to the observer.  This view does not appear as realistic as the perspective \
view, but it does have the advantage that objects of the same size at different depths line up.  \
It is useful for viewing 2-D data or for comparing values in 3-D data at different \
depths.\n"));

    var = GetIntVar("AddViewControls", observer, VIEWTYPE);
    if (!var)
    {
	SetVar(observer, VIEWTYPE, NewInt(VT_PERSPECTIVE));
    }
    AssocDirectControlWithVar(radio, observer, VIEWTYPE);

    /*Add in the perspective control*/
        control = TemplatePerspecControl(ObserverTemplate, "Perspective Control");
    PrefixList(panelContents, control);
    SetVar(control, PARENT, panelContents);
    SetVar(control, REPOBJ, observer);

    SetMethod(control, CHANGEDVALUE, ChangePerspective);
    DeclareIndirectDependency(control, APPEARANCE, REPOBJ, VIEWCLIP);
    DeclareIndirectDependency(control, APPEARANCE, REPOBJ, VIEWFIELD);
    DeclareIndirectDependency(control, APPEARANCE, REPOBJ, FOCUSDIST);
    DeclareIndirectDependency(control, APPEARANCE, REPOBJ, VIEWTYPE);
    SetMethod(control, APPEARANCE, MakePerspecControlAppearance);

    /*Reset controls*/
    button = TemplateButton(ObserverTemplate, "Reset Position");
    PrefixList(panelContents, button);
    SetVar(button, PARENT, panelContents);
    SetVar(button, REPOBJ, observer);
    SetMethod(button, CHANGEDVALUE, ResetPosition);
    SetVar(radio, HELPSTRING, NewString("This button resets the position \
of the observer to the default.  It can be useful if you get lost.\n"));

    button = TemplateButton(ObserverTemplate, "Reset Rotation");
    PrefixList(panelContents, button);
    SetVar(button, PARENT, panelContents);
    SetVar(button, REPOBJ, observer);
    SetMethod(button, CHANGEDVALUE, ResetRotation);
    SetVar(radio, HELPSTRING, NewString("This button resets the rotation \
of the observer to the default.  It can be useful if you get lost.\n"));

    GetTemplateBounds(ObserverTemplate, "Radar", &l, &r, &b, &t);
    control = NewRadarControl(l, r, b, t, "Radar");
    PrefixList(panelContents, control);
    SetVar(control, PARENT, panelContents);
    SetVar(control, REPOBJ, observer);
    SetMethod(control, CHANGEDVALUE, ChangeRadar);
    DeclareDependency(control, APPEARANCE, REPOBJ);
    DeclareIndirectDependency(control, APPEARANCE, REPOBJ, ROTQUAT);
    DeclareIndirectDependency(control, APPEARANCE, REPOBJ, LOCATION);
    SetMethod(control, APPEARANCE, MakeRadarAppearance); 

    return ObjTrue;
}

static ObjPtr AddStereoControls(observer, panelContents)
ObjPtr observer, panelContents;
/*Adds stereo observer controls to a panelContents*/
{
    ObjPtr textBox, titleBox, radio, button, control;
    int l, r, b, t;
    ObjPtr var;
    ObjPtr icon;

    titleBox = TemplateTitleBox(StereoTemplate, "Stereo Image Presentation");
    PrefixList(panelContents, titleBox);
    SetVar(titleBox, PARENT, panelContents);

    radio = NewRadioButtonGroup("Stereo Image Radio");
    PrefixList(panelContents, radio);
    SetVar(radio, PARENT, panelContents);
    SetVar(radio, REPOBJ, observer);
    SetVar(radio, HELPSTRING, NewString("This radio group controls \
the method used to display in mono or stereo all spaces controlled by this observer.  For more information \
about a given view type, use Help In Context on the button naming the view type.\n"));

    button = TemplateRadioButton(StereoTemplate, "Mono");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, only one \
image appears in the space."));

    button = TemplateRadioButton(StereoTemplate, "Crosseyed");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
images are shown side by side for a stereo pair.  The image for the left eye is \
on the right, and the image for the right eye is on the left.  To view the stereo \
pair, cross your eyes until the images coincide.  For some people, this is very \
easy to do.  Some people have difficulty doing it.  Only try this if you personally \
find it easy, and if it becomes a strain, stop at once."));

    button = TemplateRadioButton(StereoTemplate, "Walleyed");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
images are shown side by side for a stereo pair.  The image for the left eye is \
on the left, and the image for the right eye is on the right.  To view the stereo \
pair, you will need to use a stereo viewer made of a pair of small periscopes.  See the \
SciAn technical notes for more details."));

    button = TemplateRadioButton(StereoTemplate, "Red/Cyan");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
perspective images are shown, one red and one cyan, for a stereo pair.  To view the stereo \
pair, you will need to use a pair of red/cyan 3-D glasses, with the red lens over \
the left eye.  Because one eye \
can see only one set of colors, subtle color variations will be washed out.  \
Also, colors which are heavily toward red or heavily toward blue will be confusing."));
    GetTemplateBounds(StereoTemplate, "Red/Cyan", &l, &r, &b, &t);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONLGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UIRED));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONRGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UICYAN));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);

    button = TemplateRadioButton(StereoTemplate, "Cyan/Red");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
perspective images are shown, one red and one cyan, for a stereo pair.  To view the stereo \
pair, you will need to use a pair of red/cyan 3-D glasses, with the red lens over \
the right eye.  Because one eye \
can see only one set of colors, subtle color variations will be washed out.  \
Also, colors which are heavily toward red or heavily toward blue will be confusing."));
    GetTemplateBounds(StereoTemplate, "Cyan/Red", &l, &r, &b, &t);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONLGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UICYAN));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONRGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UIRED));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);

    button = TemplateRadioButton(StereoTemplate, "Red/Green");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
perspective images are shown, one red and one cyan, for a stereo pair.  To view the stereo \
pair, you will need to use a pair of red/green 3-D glasses, with the red lens over \
the left eye.  This kind of viewing is even worse than Red/Cyan for color representation and \
can effectively be used only for monochrome images."));
    GetTemplateBounds(StereoTemplate, "Red/Green", &l, &r, &b, &t);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONLGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UIRED));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONRGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UIGREEN));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);

    button = TemplateRadioButton(StereoTemplate, "Green/Red");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
perspective images are shown, one red and one cyan, for a stereo pair.  To view the stereo \
pair, you will need to use a pair of red/green 3-D glasses, with the red lens over \
the right eye.  This kind of viewing is even worse than Red/Cyan for color representation and \
can effectively be used only for monochrome images."));
    GetTemplateBounds(StereoTemplate, "Green/Red", &l, &r, &b, &t);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONLGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UIGREEN));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);
    icon = NewIcon(r - ICONSIZE / 2 - MAJORBORDER, (b + t) / 2, ICONRGLASSES, (char *) 0);
    SetVar(icon, BACKGROUND, NewInt(UIRED));
    SetMethod(icon, PRESS, (FuncTyp) 0);
    PrefixList(panelContents, icon);
    SetVar(icon, PARENT, panelContents);

    button = TemplateRadioButton(StereoTemplate, "Odd/Even");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
perspective images are shown, the image for the left eye on the odd scan lines \
and the image for the right eye on the even scan lines.  This may work in NTSC mode \
for some LCD flip glasses that switch during the vertical retrace and with some \
stereo LCD overhead projector screens.  Because of the \
way that the images must be interlaced, this will only work for images made up of polygons \
with no lines."));

    button = TemplateRadioButton(StereoTemplate, "Even/Odd");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, two \
perspective images are shown, the image for the left eye on the even scan lines \
and the image for the right eye on the odd scan lines.  This may work in NTSC mode \
for some LCD flip glasses that switch during the vertical retrace and with some \
stereo LCD overhead projector screens.  Because of the \
way that the images must be interlaced, this will only work for images made up of polygons \
with no lines."));

    button = TemplateRadioButton(StereoTemplate, "Crystal Eyes");
    AddRadioButton(radio, button);
    SetVar(button, HELPSTRING, NewString("When this button is down, stereo \
images will be displayed for use with the Crystal Eyes system.  In order for this \
to work, you must have a set of Crystal Eyes goggles.  Also, the visualization \
window must be set to full screen, and the control panel must be hidden.  Once you \
have set the visualization window to full screen, press the F11 key to toggle \
the Crystal Eyes goggles on and off."));

    var = GetIntVar("AddStereoControls", observer, STEREOMODE);
    if (!var)
    {
	SetVar(observer, STEREOMODE, NewInt(SM_MONO));
    }
    AssocDirectControlWithVar(radio, observer, STEREOMODE);

    if (!hasStereo)
    {
	ActivateButton(button, false);
    }

    /*Binocular spacing*/
    textBox = TemplateTextBox(StereoTemplate, "Binocular Legend", PLAIN, "Interocular Spacing:");
    SetVar(textBox, PARENT, panelContents);
    PrefixList(panelContents, textBox);

    var = GetRealVar("AddViewControls", observer, BINOCULARITY);
    if (!var)
    {
	SetVar(observer, BINOCULARITY, NewReal(0.4));
    }
    textBox = TemplateTextBox(StereoTemplate, "Binocular Spacing", EDITABLE + WITH_PIT + ONE_LINE, "");
    SetVar(textBox, PARENT, panelContents);
    PrefixList(panelContents, textBox);
    SetVar(textBox, HELPSTRING, NewString("This editable text box controls \
the spacing between the eyes while viewing a visualization in one of the stereo \
modes.  The larger the number, the stronger the stereo effect will be.  The \
maximum comfortable stereo effect may vary depending on the particular image, \
so you may have to experiment to get the best effect.  If you have a dial box, holding \
down the Alt key while turning the lower right dial will change this spacing."));
    SetTextAlign(textBox, RIGHTALIGN);
    AssocTextRealControlWithVar(textBox, observer, BINOCULARITY, 0.0, plusInf, 0);

    return ObjTrue;
}

static ObjPtr AddPlacementControls(observer, panelContents)
ObjPtr observer, panelContents;
/*Adds placement controls to a panelContents*/
{
    ObjPtr checkBox, textBox, titleBox, radio, control;
    ObjPtr var;

    /*Put in the controls on the right*/
    textBox = TemplateTextBox(PlacementTemplate, "X", PLAIN, "X");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, CENTERALIGN);
    textBox = TemplateTextBox(PlacementTemplate, "Y", PLAIN, "Y");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, CENTERALIGN);
    textBox = TemplateTextBox(PlacementTemplate, "Z", PLAIN, "Z");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, CENTERALIGN);

    textBox = TemplateTextBox(PlacementTemplate, "Position Text", PLAIN, "Position:");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, LEFTALIGN);

    textBox = TemplateTextBox(PlacementTemplate, "XPosn", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, USESPACECOORDS);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, SPACESCALE);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, SPACEORIGIN);
    SetVar(textBox, WHICHINDEX, NewInt(0));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverLocation);
    SetMethod(textBox, APPEARANCE, MakeObserverLocationAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
X coordinate of the observer's position.  Enter a new position and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "YPosn", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, USESPACECOORDS);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, SPACESCALE);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, SPACEORIGIN);
    SetVar(textBox, WHICHINDEX, NewInt(1));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverLocation);
    SetMethod(textBox, APPEARANCE, MakeObserverLocationAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Y coordinate of the observer's position.  Enter a new position and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "ZPosn", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, USESPACECOORDS);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, SPACESCALE);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, SPACEORIGIN);
    SetVar(textBox, WHICHINDEX, NewInt(2));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverLocation);
    SetMethod(textBox, APPEARANCE, MakeObserverLocationAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Z coordinate of the observer's position.  Enter a new position and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "Look At Text", PLAIN, "Look At:");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, LEFTALIGN);

    textBox = TemplateTextBox(PlacementTemplate, "XLookat", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, USESPACECOORDS);
    SetVar(textBox, WHICHINDEX, NewInt(0));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverLookat);
    SetMethod(textBox, APPEARANCE, MakeObserverLookatAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
X coordinate of the point at which the observer is looking.  Enter a new position and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "YLookat", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, USESPACECOORDS);
    SetVar(textBox, WHICHINDEX, NewInt(1));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverLookat);
    SetMethod(textBox, APPEARANCE, MakeObserverLookatAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Y coordinate of the point at which the observer is looking.  Enter a new position and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "ZLookat", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, USESPACECOORDS);
    SetVar(textBox, WHICHINDEX, NewInt(2));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverLookat);
    SetMethod(textBox, APPEARANCE, MakeObserverLookatAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Z coordinate of the point at which the observer is looking.  Enter a new position and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "Up Text", PLAIN, "Up Vector:");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, LEFTALIGN);

    textBox = TemplateTextBox(PlacementTemplate, "XUp", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(0));
    ActivateTextBox(textBox, false);
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverUp);
    SetMethod(textBox, APPEARANCE, MakeObserverUpAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
X coordinate of the up vector.  Enter a new coordinate and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "YUp", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(1));
    ActivateTextBox(textBox, false);
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverUp);
    SetMethod(textBox, APPEARANCE, MakeObserverUpAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Y coordinate of the up vector.  Enter a new coordinate and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "ZUp", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(2));
    ActivateTextBox(textBox, false);
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverUp);
    SetMethod(textBox, APPEARANCE, MakeObserverUpAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Z coordinate of the up vector.  Enter a new coordinate and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "Quaternion Text", PLAIN, "Rotation:");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, LEFTALIGN);

    textBox = TemplateTextBox(PlacementTemplate, "QScalar", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(0));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverQuaternion);
    SetMethod(textBox, APPEARANCE, MakeObserverQuaternionAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
scalar part of the quaternion used for rotation.  This is cos(theta/2), where \
theta is the amount of rotation.  Enter a new value and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "QX", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(1));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverQuaternion);
    SetMethod(textBox, APPEARANCE, MakeObserverQuaternionAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
X coordinate of the vector part of the quaternion used for rotation.  The \
vector points along the rotation axis with magnitude sin(theta/2), where theta \
is the amount of rotation.  Enter a new coordinate and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "QY", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(2));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverQuaternion);
    SetMethod(textBox, APPEARANCE, MakeObserverQuaternionAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Y coordinate of the vector part of the quaternion used for rotation.  The \
vector points along the rotation axis with magnitude sin(theta/2), where theta \
is the amount of rotation.  Enter a new coordinate and press the Enter \
key to change it.\n"));

    textBox = TemplateTextBox(PlacementTemplate, "QZ", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetVar(textBox, REPOBJ, observer);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, LOCATION);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FORWARDVECTOR);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, FOCUSDIST);
    SetVar(textBox, WHICHINDEX, NewInt(3));
    SetMethod(textBox, CHANGEDVALUE, ChangeObserverQuaternion);
    SetMethod(textBox, APPEARANCE, MakeObserverQuaternionAppearance);
    SetTextAlign(textBox, RIGHTALIGN);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Z coordinate of the vector part of the quaternion used for rotation.  The \
vector points along the rotation axis with magnitude sin(theta/2), where theta \
is the amount of rotation.  Enter a new coordinate and press the Enter \
key to change it.\n"));


    /*Coordinate systems*/
    radio = NewRadioButtonGroup("Coordinate System");
    PrefixList(panelContents, radio);
    SetVar(radio, PARENT, panelContents);
    SetVar(radio, HELPSTRING, NewString("This radio button group \
controls whether coordinates are displayed in observer coordinates or space \
coordinates.\n\nObserver coordinates provide a normalized \
coordinate system for viewing, where the field of view is approximately covered \
by a unit sphere centered at (0, 0, 0).  Observer coordinates are dimensionless \
and are always the same, no matter how many spaces are controlled by the observer.\n\n\
Space coordinates are the coordinates \
used by the visualization objects within the space.  These may be kilometers, angstroms, \
or whatever is appropriate.  Space coordinates can be more convenient if you \
want to go to a certain point within the space.  If an observer controls more \
than one space, the space coordinates may be different.  We recommend that you \
only use space coordinates when controlling a single space."));

    checkBox = TemplateRadioButton(PlacementTemplate, "Observer Coordinates");
    AddRadioButton(radio, checkBox);

    checkBox = TemplateRadioButton(PlacementTemplate, "Space Coordinates");
    AddRadioButton(radio, checkBox);

    AssocDirectControlWithVar(radio, observer, USESPACECOORDS);

    return ObjTrue;
}

static ObjPtr AddWorldControls(observer, panelContents)
ObjPtr observer, panelContents;
/*Adds world controls to a panelContents*/
{
    ObjPtr checkBox, textBox, titleBox, control;
    ObjPtr var;

    /*Put in the controls on the right*/
    textBox = TemplateTextBox(WorldTemplate, "X", PLAIN, "X");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, CENTERALIGN);
    textBox = TemplateTextBox(WorldTemplate, "Y", PLAIN, "Y");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, CENTERALIGN);
    textBox = TemplateTextBox(WorldTemplate, "Z", PLAIN, "Z");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, CENTERALIGN);

    textBox = TemplateTextBox(WorldTemplate, "Origin Text", PLAIN, "Origin:");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, LEFTALIGN);

    textBox = TemplateTextBox(WorldTemplate, "XOrigin", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, RIGHTALIGN);
    SetMethod(textBox, EXTRAAPPEARANCE, HideOnAutoAdjust);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, AUTOADJUST);
    AssocIndexedTextRealControlWithVar(textBox,
	observer, SPACEORIGIN, 0, minusInf, plusInf, TR_NE_BOTTOM | TR_NE_TOP);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
X coordinate of the origin of world coordinates in space coordinates.  By default, \
this is the center of all visualization objects in the space.  If the Adjust to Space \
check box is on, the origin will automatically be adjusted according to the \
objects within the space.  If the check box is off, you can change the origin \
by entering a number in the text box and pressing the Enter key."));

    textBox = TemplateTextBox(WorldTemplate, "YOrigin", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, RIGHTALIGN);
    SetMethod(textBox, EXTRAAPPEARANCE, HideOnAutoAdjust);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, AUTOADJUST);
    AssocIndexedTextRealControlWithVar(textBox,
	observer, SPACEORIGIN, 1, minusInf, plusInf, TR_NE_BOTTOM | TR_NE_TOP);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Y coordinate of the origin of world coordinates in space coordinates.  By default, \
this is the center of all visualization objects in the space.  If the Adjust to Space \
check box is on, the origin will automatically be adjusted according to the \
objects within the space.  If the check box is off, you can change the origin \
by entering a number in the text box and pressing the Enter key."));

    textBox = TemplateTextBox(WorldTemplate, "ZOrigin", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, RIGHTALIGN);
    SetMethod(textBox, EXTRAAPPEARANCE, HideOnAutoAdjust);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, AUTOADJUST);
    AssocIndexedTextRealControlWithVar(textBox,
	observer, SPACEORIGIN, 2, minusInf, plusInf, TR_NE_BOTTOM | TR_NE_TOP);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
Y coordinate of the origin of world coordinates in space coordinates.  By default, \
this is the center of all visualization objects in the space.  If the Adjust to Space \
check box is on, the origin will automatically be adjusted according to the \
objects within the space.  If the check box is off, you can change the origin \
by entering a number in the text box and pressing the Enter key."));

    /*Scaling*/
    textBox = TemplateTextBox(WorldTemplate, "Scaling Text", PLAIN, "Scaling:");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, LEFTALIGN);

    textBox = TemplateTextBox(WorldTemplate, "Scale X", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, RIGHTALIGN);
    SetMethod(textBox, EXTRAAPPEARANCE, HideOnAutoAdjust);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, AUTOADJUST);
    AssocIndexedTextRealControlWithVar(textBox,
	observer, SPACESCALE, 0, 0.0, plusInf, TR_NE_BOTTOM | TR_NE_TOP);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
scaling of the worlds's X axis.  This is the amount \
in space coordinates that corresponds to 1 unit in world coordinates.  By default, \
the scaling is set so that all the objects in the space fit roughly within a \
sphere of radius 1 centered on the origin.  If the Adjust to Space \
check box is on, the scaling will automatically be adjusted according to the \
objects within the space.  If the check box is off, you can change the scaling \
by entering a number in the text box and pressing the Enter key."));

    textBox = TemplateTextBox(WorldTemplate, "Scale Y", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, RIGHTALIGN);
    SetMethod(textBox, EXTRAAPPEARANCE, HideOnAutoAdjust);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, AUTOADJUST);
    AssocIndexedTextRealControlWithVar(textBox,
	observer, SPACESCALE, 1, 0.0, plusInf, TR_NE_BOTTOM | TR_NE_TOP);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
scaling of the world's Y axis.  This is the amount \
in space coordinates that corresponds to 1 unit in world coordinates.  By default, \
the scaling is set so that all the objects in the space fit roughly within a \
sphere of radius 1 centered on the origin.  If the Adjust to Space \
check box is on, the scaling will automatically be adjusted according to the \
objects within the space.  If the check box is off, you can change the scaling \
by entering a number in the text box and pressing the Enter key."));

    textBox = TemplateTextBox(WorldTemplate, "Scale Z", EDITABLE + WITH_PIT + ONE_LINE, "");
    PrefixList(panelContents, textBox);
    SetVar(textBox, PARENT, panelContents);
    SetTextAlign(textBox, RIGHTALIGN);
    SetMethod(textBox, EXTRAAPPEARANCE, HideOnAutoAdjust);
    DeclareIndirectDependency(textBox, APPEARANCE, REPOBJ, AUTOADJUST);
    AssocIndexedTextRealControlWithVar(textBox,
	observer, SPACESCALE, 2, 0.0, plusInf, TR_NE_BOTTOM | TR_NE_TOP);
    SetVar(textBox, HELPSTRING, NewString("This text box controls the \
scaling of the world's Z axis.  This is the amount \
in space coordinates that corresponds to 1 unit in world coordinates.  By default, \
the scaling is set so that all the objects in the space fit roughly within a \
sphere of radius 1 centered on the origin.  If the Adjust to Space \
check box is on, the scaling will automatically be adjusted according to the \
objects within the space.  If the check box is off, you can change the scaling \
by entering a number in the text box and pressing the Enter key."));

    /*Adjust to space*/
    checkBox = TemplateCheckBox(WorldTemplate, "Adjust to Space", false);
    PrefixList(panelContents, checkBox);
    SetVar(checkBox, PARENT, panelContents);
    AssocDirectControlWithVar(checkBox, observer, AUTOADJUST);
    SetVar(checkBox, HELPSTRING, NewString("This check box controls \
whether the observer's origin and scaling are automatically adjusted to the space being \
drawn.  When this box is off, you can enter the origin and scaling manually.  \
When it is on, the origin and scaling will change depending on which space is \
being drawn.  If the observer controls several spaces with different coordinate \
systems, this may result in the \
numbers changing rapidly as the individual spaces are drawn."));

    return ObjTrue;
}

static ObjPtr ShowObserverControls(object, windowName)
ObjPtr object;
char *windowName;
/*Makes a new control window to control observer object*/
{
    WinInfoPtr controlWindow;
    ObjPtr var;
    ObjPtr panel;
    ObjPtr controlField;
    ObjPtr contents;
    ObjPtr curObj;
    ObjPtr firstButton = NULLOBJ;
    int left, right, bottom, top, width;
    WinInfoPtr dialogExists;
    Bool abortp = false;
    Bool saveSettings = false;

    dialogExists = DialogExists((WinInfoPtr) object, NewString("Controls"));
    controlWindow = GetDialog((WinInfoPtr) object, NewString("Controls"), windowName, 
	SMCWINWIDTH, SMCWINHEIGHT, SMCWINWIDTH, SMCWINHEIGHT, WINFIXEDSIZE + WINUI);
    
    if (!dialogExists)
    {
	SetVar((ObjPtr) controlWindow, REPOBJ, object);

	/*Add in help string*/
	SetVar((ObjPtr) controlWindow, HELPSTRING,
	    NewString("This window shows controls for an observer.  The observer \
represents you, looking into the space containing visualization objects.  The controls \
allow you to change your viewing angle, location, orientation, and clipping planes.\n\n\
At the right is an icon corral showing a series of icons.  Each icon represents a \
set of attributes of the observer.  On the left are the controls for \
the selected set of attributes.  \
Use Help In Context and click on the various controls to find out what they do.  \
Click on a different icon to choose a different set of attributes."));

	/*Add in a panel*/
	panel = NewPanel(greyPanelClass, 0, SMCWINWIDTH, 0, SMCWINHEIGHT);
	if (!panel)
	{
	    return ObjFalse;
	}
	contents = GetVar((ObjPtr) controlWindow, CONTENTS);
	PrefixList(contents, panel);
	SetVar(panel, PARENT, (ObjPtr) controlWindow);

	contents = GetVar(panel, CONTENTS);

	/*Add in a control field*/
	controlField = NewControlField(SMCWINWIDTH - CORRALBORDER - SMCWINCORRALWIDTH,
			       SMCWINWIDTH - CORRALBORDER,
			       CORRALBORDER,
			       SMCWINHEIGHT - CORRALBORDER,
				"Observer Attributes", OBJECTSFROMTOP | BARRIGHT);
	SetVar(controlField, HELPSTRING,
	   NewString("This icon button group shows sets of attributes of the observer \
object that can be modified.  The left side of the panel shows controls for the \
attribute given by the selected icon button.  To show another set of \
attributes, press another button."));
	SetVar(controlField, PARENT, panel);
	PrefixList(contents, controlField);

	contents = GetVar(controlField, CONTENTS);

	/*Fill the control field up with buttons*/
	curObj = object;
	top = -MAJORBORDER;
	width = SMCWINCCWIDTH;

	saveSettings = GetPredicate(curObj, CANSAVESETTINGS);

	while (curObj)
	{
	    ObjPtr icon;
	    icon = Get1Var(curObj, CONTROLICON);
	    if (icon)
	    {
		ObjPtr button;
		ObjPtr panelContents;
		FuncTyp method;
		int whichIcon;
		char *name;

		if (firstButton && abortp) break;

		var = GetIntVar("ShowObserverControls", icon, WHICHICON);
		if (var)
		{
		    whichIcon = GetInt(var);
		}
		else
		{
		    whichIcon = ICONQUESTION;
		}

		var = GetStringVar("ShowObserverControls", icon, NAME);
		if (var)
		{
		    name = GetString(var);
		}
		else
		{
		    name = "Unknown";
		}

		button = NewIconLabeledButton(0, width, top - SMCWINICONBUTHEIGHT, top,
			whichIcon, UIYELLOW, name, BS_PITTED);
		SetMethod(button, ICONEXTRADRAW, GetMethod(icon, ICONEXTRADRAW));
		SetVar(button, REPOBJ, object);
		SetMethod(button, CHANGEDVALUE, ChangeControlPanelButton);

		SetVar(button, PANEL, panel);

		if (!firstButton)
		{
		    firstButton = button;
		}

		if (saveSettings)
		{
		    SetVar(button, CANSAVESETTINGS, ObjTrue);
		}
		/*Make a new panel contents just for this button*/
		panelContents = NewList();
		SetVar(panelContents, TYPESTRING, NewString("control panel"));
		SetVar(panelContents, NAME, NewString(name));
		if (var = GetVar(icon, PANELHELP))
		{
		    ObjPtr iconButton;
		    SetVar(panelContents, HELPSTRING, var);
		}
		PrefixList(panelContents, controlField);
		SetVar(panelContents, PARENT, panel);
		SetVar(button, PANELCONTENTS, panelContents);
		SetVar(button, PARENT, panelContents);

		/*Give the button a chance to add controls*/
		method = Get1Method(curObj, ADDCONTROLS);
		if (method)
		{
		    SetVar(button, CONTROLSADDED, ObjFalse);
		    SetMethod(button, ADDCONTROLS, method);
		}
		else
		{
		    SetVar(button, CONTROLSADDED, ObjTrue);
		}
		PrefixList(contents, button);
		SetVar(button, PARENT, controlField);
		top -= SMCWINICONBUTHEIGHT + MINORBORDER;
    
	    }
	    if (GetPredicate(curObj, ABORTCONTROLS)) abortp = true;
	    curObj = ClassOf(curObj);
	}

	/*Adjust the scroll bars*/
	RecalcScroll(controlField);

	if (firstButton)
	{
	    SetValue(firstButton, NewInt(1));
	    SetVar(controlField, BUTTON, firstButton);
	}
    }

    return (ObjPtr) controlWindow;
}

ObjPtr ShowRendererControls(renderer, windowName)
ObjPtr renderer;
ObjPtr windowName;
/*Makes a new renderer window to control renderer.*/
{
    WinInfoPtr rendererWindow;
    ObjPtr name;
    ObjPtr var;
    ObjPtr panel;
    ObjPtr contents;
    WinInfoPtr dialogExists;
    ObjPtr whichDialog;
    int left, right, bottom, top;

    if (!renderer) return NULLOBJ;

    name = GetVar(renderer, NAME);

    whichDialog = NewString("Renderer");
    dialogExists = DialogExists((WinInfoPtr) renderer, whichDialog);
    if (name)
    {
	strncpy(tempStr, GetString(name), TEMPSTRSIZE);
	tempStr[TEMPSTRSIZE] = 0;
    }
    else
    {
	strcpy(tempStr, "Renderer");
    }
    GetTemplateBounds(RendererTemplate, "Panel", &left, &right, &bottom, &top);

    rendererWindow = GetDialog((WinInfoPtr) renderer, whichDialog, tempStr, 
	right - left, top - bottom, right - left, top - bottom, WINUI);

    if (!dialogExists)
    {
	SetVar((ObjPtr) rendererWindow, REPOBJ, renderer);

	/*Add controls*/
	SetVar((ObjPtr) rendererWindow, HELPSTRING,
	    NewString("This window shows controls that affect the rendering of \
the objects within the space."));
	contents = GetVar((ObjPtr) rendererWindow, CONTENTS);

	/*Add in a panel*/
	panel = TemplatePanel(RendererTemplate, "Panel");
	if (!panel)
	{
	    return ObjFalse;
	}
	PrefixList(contents, panel);
	SetVar(panel, PARENT, (ObjPtr) rendererWindow);

	contents = GetListVar("ShowRendererControls", panel, CONTENTS);
	if (contents)
	{
	    ObjPtr titleBox;
	    ObjPtr button;
	    ObjPtr radio;
	    ObjPtr var;

	    /*Make the title box around render type*/
	    titleBox = TemplateTitleBox(RendererTemplate, "Renderer Type");
	    PrefixList(contents, titleBox);
	    SetVar(titleBox, PARENT, panel);

	    /*Make the no renderer button*/
	    radio = NewRadioButtonGroup("Renderer");
	    SetVar(radio, HELPSTRING, NewString("These radio buttons control what kind of renderer is \
used to render the objects within the space.  At present, there is only one renderer available: a hardware renderer."));

	    button = TemplateRadioButton(RendererTemplate, "No Renderer");
	    AddRadioButton(radio, button);
	    SetVar(button, HELPSTRING,
		NewString("This button causes rendering not to be done on the space.  \
This is sometimes useful as to hide all the visualization objects."));

	    /*Make the full color hardware renderer button*/
	    button = TemplateRadioButton(RendererTemplate,  "Full Color Hardware");
	    AddRadioButton(radio, button);
	    SetVar(button, HELPSTRING,
		NewString("This button sets the space to use the hardware renderer \
in full color mode.  In this mode, all the lighting and filtering is available.  Full color \
mode only works if the hardware has enough bitplanes to support it."));
	    if (!hasRGB)
	    {
		ActivateButton(button, false);
	    }	

	    /*Make the color map hardware renderer button*/
	    button = TemplateRadioButton(RendererTemplate,  "Color Map Hardware");
	    AddRadioButton(radio, button);
	    SetVar(button, HELPSTRING,
		NewString("This button sets the space to use the hardware renderer \
in color map mode.  In this mode, lighting and image filtering is not available, but it can be useful \
on simple color shaded visualizations.  Color map mode only works on hardware with \
color maps."));
	    if (!hasCmap)
	    {
		ActivateButton(button, false);
	    }	

	    /*Set the radio button group*/
	    PrefixList(contents, radio);
	    SetVar(radio, PARENT, panel);

	    var = GetVar(renderer, RENDERTYPE);
	    if (!var)
	    {
		SetVar(renderer, RENDERTYPE, NewInt(hasRGB ? RT_RGB_HARDWARE : RT_CMAP_HARDWARE));
	    }
	    AssocDirectControlWithVar(radio, renderer, RENDERTYPE);

	    /*Make the title box around filter type*/
	    titleBox = TemplateTitleBox(RendererTemplate, "Image Filter");
	    PrefixList(contents, titleBox);
	    SetVar(titleBox, PARENT, panel);

	    /*Make the no filter button*/
	    radio = NewRadioButtonGroup("Filter Type");
	    PrefixList(contents, radio);
	    SetVar(radio, PARENT, panel);

	    SetVar(radio, HELPSTRING,
		NewString("These radio buttons select the kind of filtration that is done \
to the image of the space after it has been rendererd.  Filtration only works when the \
space is set to full color mode."));
	    button = TemplateRadioButton(RendererTemplate, "No Filter");
	    AddRadioButton(radio, button);
	    SetVar(button, HELPSTRING,
		NewString("This button causes the image of the space to be shown \
without filtration.  This is the fastest and is recommended for interactive work."));

	    /*Make the shrink filter button*/
	    button = TemplateRadioButton(RendererTemplate, "2 to 1 shrink");
	    SetVar(button, HELPSTRING,
		NewString("This button causes the image of the space to be shrunk \
2 to 1 and averaged before being displayed.  This only works well when the window is \
in full color mode and the entire window is shown on the screen and is not covered by \
any other window.  It produces pretty good results with video.  Use the Double Video Screen \
item in the Window menu."));
	    AddRadioButton(radio, button);
	    if (!hasRGB)
	    {
		ActivateButton(button, false);
	    }	

	    /*Make the shrink filter button*/
	    button = TemplateRadioButton(RendererTemplate, "Four neighbor average");
	    SetVar(button, HELPSTRING,
		NewString("This button causes each pixel of image of the space to be \
averaged with its four neighbors in the horizontal and vertical directions before being displayed.  \
This only works well when the window is in full color mode and is not covered by \
other windows.  It produces pretty good results with video, \
though not as good as the shrinking filter."));
	    AddRadioButton(radio, button);
	    if (!hasRGB)
	    {
		ActivateButton(button, false);
	    }	

	    SetVar(radio, REPOBJ, renderer);
	    var = GetIntVar("ShowRendererControls", renderer, FILTERTYPE);
	    if (!var)
	    {
		SetVar(renderer, FILTERTYPE, NewInt(FT_NONE));
	    }
	    AssocDirectControlWithVar(radio, renderer, FILTERTYPE);
	}
    }

    return (ObjPtr) rendererWindow;
}

ObjPtr MakeClockTimeFormat(clock)
ObjPtr clock;
/*Makes a clock's TIMEFORMAT variable*/
{
    ObjPtr datasets;
    int curFormat;
    ObjPtr format;
    ObjPtr element;
    long k;

    MakeVar(clock, DATASETS);
    curFormat = 0;

    datasets = GetVar(clock, DATASETS);
    if (datasets)
    {
	for (k = 0; k < DIMS(datasets)[0]; ++k)
	{
	    element = GetObjectElement(datasets, &k);
	    MakeVar(element, TIMEFORMAT);
	    format = GetVar(element, TIMEFORMAT);
	    if (format)
	    {
		int newFormat;
		newFormat = GetInt(format);
		curFormat |= newFormat;
	    }
	}
    }
    if (!curFormat)
    {
	curFormat = TF_SECONDS + TF_SUBSECONDS;
    }
    SetVar(clock, TIMEFORMAT, NewInt(curFormat));
    return ObjTrue;
}

ObjPtr MakeClockDatasets(clock)
ObjPtr clock;
/*Makes a clock's DATASETS variable, along with time slices*/
{
    ObjPtr list, spaces, array = NULLOBJ;
    ObjPtr element;
    long k;
    WinInfoPtr dialog;

    list = NewList();
    spaces = GetListVar("MakeClockDatasets", clock, SPACES);
    if (spaces)
    {
	ThingListPtr spaceRunner;
	spaceRunner = LISTOF(spaces);
	while (spaceRunner)
	{
	    ObjPtr contents;
	    contents = GetListVar("MakeClockDatasets", spaceRunner -> thing, CONTENTS);
	    if (contents)
	    {
		ThingListPtr contentsRunner;
		contentsRunner = LISTOF(contents);
		while (contentsRunner)
		{
		    PrefixDatasets(list, contentsRunner -> thing);
		    contentsRunner = contentsRunner -> next;
		}
	    }
	    spaceRunner = spaceRunner -> next;
	}
    }

    if (LISTOF(list))
    {
	/*There's something in the list*/
	Bool hasMinValue = false;
	real minValue;

	/*Convert to an array*/
	array = ListToArray(list);

	/*Remove duplicate elements*/
	array = Uniq(array);

	/*Sort the array*/
	array = SortArrayByStringVar(array, NAME);

	for (k = 0; k < DIMS(array)[0]; ++k)
	{
	    ObjPtr name, timeSteps;
	    element = GetObjectElement(array, &k);

	    MakeVar(element, NAME);
	    MakeVar(element, TIMESTEPS);
	    name = GetVar(element, NAME);
	    timeSteps = GetVar(element, TIMESTEPS);
	    if (timeSteps)
	    {
		if (hasMinValue)
		{
		    minValue = MIN(minValue, *((real *)ELEMENTS(timeSteps)));
		}
		else
		{
		    minValue = *((real *)ELEMENTS(timeSteps));
		    hasMinValue = true;
		}
	    }
	}
	if (hasMinValue)
	{
	    if (!GetVar(clock, TIME))
	    {
		SetClock(clock, minValue);
	    }
	}
	else
	{
	    SetVar(clock, TIME, NULLOBJ);
	    ImInvalid(clock);
	}
    }
    else
    {
	SetVar(clock, TIME, NULLOBJ);
	ImInvalid(clock);
    }

    /*Set the clock's datasets to datasets*/
    SetVar(clock, DATASETS, array);

    return ObjTrue;
}

ObjPtr commonClock = NULLOBJ;
ObjPtr commonObserver = NULLOBJ;
ObjPtr commonRenderer = NULLOBJ;

int clockNum = 0;
int observerNum = 0;
int rendererNum = 0;

ObjPtr NewClock()
/*Returns a new clock*/
{
    ObjPtr retVal;
    ObjPtr name;


    if (oneClock)
    {
	if (commonClock) return commonClock;
    }

    retVal = NewObject(clockClass, 0);
    if (oneClock)
    {
	commonClock = retVal;
    }
    name = GetVar(clockClass, NAME);
    if (name)
    {
	sprintf(tempStr, "%s %d", GetString(name), ++clockNum);
    }
    else
    {
	sprintf(tempStr, "Clock %d", ++clockNum);
    }
    name = NewString(tempStr);
    SetVar(retVal, NAME, name);
    SetVar(retVal, SPACES, NewList());
    SetVar(retVal, CYCLECLOCK, ObjTrue);
    return retVal;
}

#ifdef HAVE_PROTOTYPES
void GetUpVector(real upVector[3], ObjPtr observer)
#else
void GetUpVector(upVector, observer)
real upVector[3];
ObjPtr observer;
#endif
/*Gets the up vector of an observer*/
{
    ObjPtr var;

    MakeVar(observer, UPVECTOR);
    var = GetVar(observer, UPVECTOR);
    if (var)
    {
	Array2CArray(upVector, var);
    }
    else
    {
	upVector[0] = 0.0;
	upVector[1] = 1.0;
	upVector[2] = 0.0;
    }
}

#ifdef HAVE_PROTOTYPES
void GetForwardVector(real forwardVector[3], ObjPtr observer)
#else
void GetForwardVector(forwardVector, observer)
real forwardVector[3];
ObjPtr observer;
#endif
/*Gets the forward vector of an observer*/
{
    ObjPtr var;

    MakeVar(observer, FORWARDVECTOR);
    var = GetVar(observer, FORWARDVECTOR);
    if (var)
    {
	Array2CArray(forwardVector, var);
    }
    else
    {
	forwardVector[0] = 0.0;
	forwardVector[1] = 0.0;
	forwardVector[2] = -1.0;
    }
}

static ObjPtr MakeObserverForwardVector(observer)
ObjPtr observer;
/*Makes the observer's forward vector*/
{
    real forwardVector[3];
    Quaternion rotQuat;
    ObjPtr var;

    MakeVar(observer, ROTQUAT);
    if (var = GetVar(observer, ROTQUAT))
    {
	/*There's a rotation quaternion, derive forward from that*/
	real temp[3];

	temp[0] = 0.0;
	temp[1] = 0.0;
	temp[2] = -1.0;

	Array2CArray(rotQuat, var);
	QRot(temp, rotQuat, forwardVector);
    }
    else
    {
	forwardVector[0] = 0.0;
	forwardVector[1] = 0.0;
	forwardVector[2] = -1.0;
    }

    var = NewRealArray(1, 3L);
    CArray2Array(var, forwardVector);
    SetVar(observer, FORWARDVECTOR, var);
    return ObjTrue;
}

static ObjPtr MakeObserverUpVector(observer)
ObjPtr observer;
/*Makes the observer's up vector*/
{
    real upVector[3];
    Quaternion rotQuat;
    ObjPtr var;

    MakeVar(observer, ROTQUAT);
    if (var = GetVar(observer, ROTQUAT))
    {
	/*There's a rotation quaternion, derive forward from that*/
	real temp[3];

	temp[0] = 0.0;
	temp[1] = 1.0;
	temp[2] = 0.0;

	Array2CArray(rotQuat, var);
	QRot(temp, rotQuat, upVector);
    }
    else
    {
	upVector[0] = 0.0;
	upVector[1] = 1.0;
	upVector[2] = 0.0;
    }
    var = NewRealArray(1, 3L);
    CArray2Array(var, upVector);
    SetVar(observer, UPVECTOR, var);
    return ObjTrue;
}

#ifdef HAVE_PROTOTYPES
void GetAdjustedVectors(real forwardVector[3], real upVector[3], ObjPtr observer)
#else
void GetAdjustedVectors(forwardVector, upVector, observer)
real forwardVector[3];
real upVector[3];
ObjPtr observer;
#endif
/*Gets the vectors of an observer, adjusted for roll and pitch*/
{
    ObjPtr var;
    real temp[3];
    real sp, cp, t;
    Matrix rotDelta;
    real phi;

    GetUpVector(upVector, observer);
    GetForwardVector(forwardVector, observer);

    var = GetVar(observer, PITCH);
    if (var)
    {
	phi = GetReal(var);
	if (phi != 0.0)
	{
	    /*Rotate down around pitch*/
	    real side[3];

	    sp = rsin(phi);
	    cp = rcos(phi);
	    t = 1.0 - cp;
	
	    /*Get a side vector*/
	    CROSS(forwardVector, upVector, side);

	    /*Make the change rotation matrix by rows from top to bottom*/
	    rotDelta[0][0] = t * SQUARE(side[0]) + cp;
	    rotDelta[0][1] = t * side[0] * side[1] + sp * side[2];
	    rotDelta[0][2] = t * side[0] * side[2] - sp * side[1];
	    rotDelta[0][3] = 0.0;
	
	    rotDelta[1][0] = t * side[0] * side[1] - sp * side[2];
	    rotDelta[1][1] = t * SQUARE(side[1]) + cp;
	    rotDelta[1][2] = t * side[1] * side[2] + sp * side[0];
	    rotDelta[1][3] = 0.0;
	
	    rotDelta[2][0] = t * side[0] * side[2] + sp * side[1];
	    rotDelta[2][1] = t * side[1] * side[2] - sp * side[0];
	    rotDelta[2][2] = t * SQUARE(side[2]) + cp;
	    rotDelta[2][3] = 0.0;
	
	    rotDelta[3][0] = 0.0;
	    rotDelta[3][1] = 0.0;
	    rotDelta[3][2] = 0.0;
	    rotDelta[3][3] = 1.0;
	
	    /*Rotate forward and up down through pitch*/

	    MATBYVECTOR(rotDelta, forwardVector, temp);
	    VECCOPY(forwardVector, temp);

	    MATBYVECTOR(rotDelta, upVector, temp);
	    VECCOPY(upVector, temp);
	}
    }

    var = GetVar(observer, ROLL);
    if (var)
    {
	phi = -GetReal(var);
	if (phi != 0.0)
	{
	    /*Rotate around -roll*/
	    sp = rsin(phi);
	    cp = rcos(phi);
	    t = 1.0 - cp;
	
	    /*Make the change rotation matrix by rows from top to bottom*/
	    rotDelta[0][0] = t * SQUARE(forwardVector[0]) + cp;
	    rotDelta[0][1] = t * forwardVector[0] * forwardVector[1] + sp * forwardVector[2];
	    rotDelta[0][2] = t * forwardVector[0] * forwardVector[2] - sp * forwardVector[1];
	    rotDelta[0][3] = 0.0;
	
	    rotDelta[1][0] = t * forwardVector[0] * forwardVector[1] - sp * forwardVector[2];
	    rotDelta[1][1] = t * SQUARE(forwardVector[1]) + cp;
	    rotDelta[1][2] = t * forwardVector[1] * forwardVector[2] + sp * forwardVector[0];
	    rotDelta[1][3] = 0.0;
	
	    rotDelta[2][0] = t * forwardVector[0] * forwardVector[2] + sp * forwardVector[1];
	    rotDelta[2][1] = t * forwardVector[1] * forwardVector[2] - sp * forwardVector[0];
	    rotDelta[2][2] = t * SQUARE(forwardVector[2]) + cp;
	    rotDelta[2][3] = 0.0;
	
	    rotDelta[3][0] = 0.0;
	    rotDelta[3][1] = 0.0;
	    rotDelta[3][2] = 0.0;
	    rotDelta[3][3] = 1.0;
	
	    /*Rotate up around through roll*/
	    MATBYVECTOR(rotDelta, upVector, temp);
	    VECCOPY(upVector, temp);
	}
    }
}

#ifdef HAVE_PROTOTYPES
void GetObserverLocation(real *location, ObjPtr observer)
#else
void GetObserverLocation(location, observer)
real *location;
ObjPtr observer;
#endif
/*Gets an observer's location*/
{
    ObjPtr var;
    var = GetVar(observer, LOCATION);
    if (var)
    {
	Array2CArray(location, var);
    }
    else
    {
	location[0] = 0.0;
	location[1] = 0.0;
	location[2] = INITEYEDIST;
    }
}

#ifdef HAVE_PROTOTYPES
void GetFocusPoint(real *focusPoint, ObjPtr observer)
#else
void GetFocusPoint(focusPoint, observer)
real *focusPoint;
ObjPtr observer;
#endif
/*Gets the focus point of an observer, adjusted for roll and pitch*/
{
    real forward[3], up[3];
    ObjPtr var;
    real focusDist;

    GetAdjustedVectors(forward, up, observer);
    GetObserverLocation(focusPoint, observer);
    var = GetVar(observer, FOCUSDIST);
    if (var)
    {
	focusDist = GetReal(var);
    }
    else
    {
	focusDist = INITEYEDIST;
    }
    focusPoint[0] += focusDist * forward[0];
    focusPoint[1] += focusDist * forward[1];
    focusPoint[2] += focusDist * forward[2];
}

ObjPtr NewObserver()
/*Returns a new observer*/
{
    ObjPtr retVal;
    ObjPtr name;

    if (oneObserver)
    {
	if (commonObserver) return commonObserver;
    }
    retVal = NewObject(observerClass, 0);
    if (oneObserver)
    {
	commonObserver = retVal;
    }
    name = GetVar(observerClass, NAME);
    if (name)
    {
	sprintf(tempStr, "%s %d", GetString(name), ++observerNum);
    }
    else
    {
	sprintf(tempStr, "Observer %d", ++observerNum);
    }
    name = NewString(tempStr);
    SetVar(retVal, NAME, name);
    SetVar(retVal, SPACES, NewList());

    return retVal;
}

ObjPtr NewRenderer()
/*Returns a new renderer*/
{
    ObjPtr retVal;
    ObjPtr name;

    if (oneRenderer)
    {
	if (commonRenderer) return commonRenderer;
    }
    retVal = NewObject(rendererClass, 0);
    if (oneRenderer)
    {
	commonRenderer = retVal;
    }
    name = GetVar(rendererClass, NAME);
    if (name)
    {
	sprintf(tempStr, "%s %d", GetString(name), ++rendererNum);
    }
    else
    {
	sprintf(tempStr, "Renderer %d", ++rendererNum);
    }
    name = NewString(tempStr);
    SetVar(retVal, NAME, name);
    SetVar(retVal, SPACES, NewList());

    return retVal;
}

static ObjPtr CloneObserver(observer)
ObjPtr observer;
/*Clones observer*/
{
    Bool oldOneObserver;
    ObjPtr retVal;

    oldOneObserver = oneObserver;
    oneObserver = false;
    retVal = NewObserver();
    oneObserver = oldOneObserver;

    CopyVar(retVal, observer, ROTAXIS);
    CopyVar(retVal, observer, ROTSPEED);	
    CopyVar(retVal, observer, VIEWFIELD);
    CopyVar(retVal, observer, VIEWCLIP);
    CopyVar(retVal, observer, LOCATION);
    CopyVar(retVal, observer, FORWARDVECTOR);
    CopyVar(retVal, observer, FOCUSDIST);
    CopyVar(retVal, observer, ROLL);
    CopyVar(retVal, observer, PITCH);
    CopyVar(retVal, observer, AIRSPEED);

    return retVal;
}

static ObjPtr CloneClock(clock)
ObjPtr clock;
{
    ObjPtr retVal;
    Bool oldOneClock;

    oldOneClock = oneClock;
    oneClock = false;
    retVal = NewClock();
    oneClock = oldOneClock;

    CopyVar(retVal, clock, CYCLECLOCK);
    CopyVar(retVal, clock, TIMESTEPS);
    CopyVar(retVal, clock, DATASETS);
    CopyVar(retVal, clock, TIME);

    return retVal;
}

static ObjPtr CloneRenderer(renderer)
ObjPtr renderer;
/*Clones renderer*/
{
    Bool oldOneRenderer;
    ObjPtr retVal;

    oldOneRenderer = oneRenderer;
    oneRenderer = false;
    retVal = NewRenderer();
    oneRenderer = oldOneRenderer;

    CopyVar(retVal, renderer, RENDERTYPE);
    CopyVar(retVal, renderer, FILTERTYPE);

    return retVal;
}

#if 0
static ObjPtr DrawExtraObserverIcon(icon, x, y)
ObjPtr icon;
int x, y;
/*Draws the extra stuff for an observer icon*/
{
    ObjPtr space, observers;
    space = GetObjectVar("DrawExtraObserverIcon", icon, SPACE);
    if (!space)
    {
	return ObjFalse;
    }
    observers = GetListVar("DrawExtraObserverIcon", space, OBSERVERS);
    if (!observers)
    {
	return ObjFalse;
    }
    if (GetVar(icon, REPOBJ) == LISTOF(observers) -> thing)
    {
	FrameUIRect(x - ICONSIZE / 2 - 4, x + ICONSIZE / 2 + 4, y - ICONSIZE / 2 - ICONSHADOW - 4, y + ICONSIZE / 2 + 4, UIREC);
	SetUIColor(UIBLACK);
    }
    return ObjTrue;
}
#endif

ObjPtr ReshapeSpacePanel(object, ol, or, ob, ot, left, right, bottom, top)
ObjPtr object;
int ol, or, ob, ot;
int left, right, bottom, top;
/*Reshapes a space panel, which used to exist within owner with edges ol, or, ob, ot
  to one which exists within owner with edges left, right, bottom, top.*/
{
    ObjPtr boundsArray;
    ObjPtr stickyInt;
    real bounds[4];
    real oldWidth, oldHeight;	/*Old width and height*/
    Bool sideLocked[4];		/*True iff side is locked*/
    Bool xStretch, yStretch;	/*Stretchiness in x and y*/
    int stickiness;		/*Side stickiness of the object*/
    real oldBounds[4];		/*Old bounds of the object*/
    ObjPtr contents;		/*Contents of the object, if any*/
    real wr, hr;		/*Width and height ratios*/

    /*Reshape the panel itself*/
    wr = ((real) (right - left)) / ((real) (or - ol));
    hr = ((real) (top - bottom)) / ((real) (ot - ob));

    MakeVar(object, BOUNDS);
    boundsArray = GetVar(object, BOUNDS);
    if (!boundsArray || !IsRealArray(boundsArray) || RANK(boundsArray) != 1 ||
	DIMS(boundsArray)[0] != 4)
    {
	return ObjFalse;
    }
    Array2CArray(bounds, boundsArray);
    Array2CArray(oldBounds, boundsArray);
    oldWidth = bounds[1] - bounds[0];
    oldHeight = bounds[3] - bounds[2];

    /*Get the object's stickiness*/
    stickyInt = GetVar(object, STICKINESS);
    if (stickyInt && IsInt(stickyInt))
    {
	stickiness = GetInt(stickyInt);
    }
    else
    {
	stickiness = 0;
    }

    if ((stickiness & STICKYLEFT) || (stickiness & FLOATINGLEFT))
    {
	if (stickiness & FLOATINGLEFT)
	{
	    bounds[0] = (bounds[0] - ol) * wr + left;
	}
	else
	{
	    bounds[0] += left - ol;
	}
	if (!((stickiness & STICKYRIGHT) || (stickiness & FLOATINGRIGHT)))
	{
	    bounds[1] = bounds[0] + oldWidth;
	}
    }
    if ((stickiness & STICKYRIGHT) || (stickiness & FLOATINGRIGHT))
    {
	if (stickiness & FLOATINGRIGHT)
	{
	    bounds[1] = (bounds[1] - ol) * wr + left;
	}
	else
	{
	    bounds[1] += right - or;
	}
	if (!((stickiness & STICKYLEFT) || (stickiness & FLOATINGLEFT)))
	{
	    bounds[0] = bounds[1] - oldWidth;
	}
    }

    if ((stickiness & STICKYBOTTOM) || (stickiness & FLOATINGBOTTOM))
    {
	if (stickiness & FLOATINGBOTTOM)
	{
	    bounds[2] = (bounds[2] - ob) * hr + bottom;
	}
	else
	{
	    bounds[2] += bottom - ob;
	}
	if (!((stickiness & STICKYTOP) || (stickiness & FLOATINGTOP)))
	{
	    bounds[3] = bounds[2] + oldHeight;
	}
    }
    if ((stickiness & STICKYTOP) || (stickiness & FLOATINGTOP))
    {
	if (stickiness & FLOATINGTOP)
	{
	    bounds[3] = (bounds[3] - ob) * hr + bottom;
	}
	else
	{
	    bounds[3] += top - ot;
	}
	if (!((stickiness & STICKYBOTTOM) || (stickiness & FLOATINGBOTTOM)))
	{
	    bounds[2] = bounds[3] - oldHeight;
	}
    }

    /*We've got a new bounds, put it back*/
    boundsArray = NewRealArray(1, 4L);
    CArray2Array(boundsArray, bounds);
    SetVar(object, BOUNDS, boundsArray);
 
    /*If there are some contents to this, reshape them*/
    contents = GetVar(object, CONTENTS);
    if (contents && IsList(contents))
    {
	ThingListPtr runner;
	real oldCenterX, oldCenterY, centerX, centerY;
	real enlargement;
	
	oldCenterX = ((real) (oldBounds[1] - oldBounds[0])) * 0.5;
	oldCenterY = ((real) (oldBounds[3] - oldBounds[2])) * 0.5;
	centerX = ((real) (bounds[1] - bounds[0])) * 0.5;
	centerY = ((real) (bounds[3] - bounds[2])) * 0.5;
	enlargement = ((real) (bounds[3] - bounds[2])) /
		      ((real) (oldBounds[3] - oldBounds[2]));

	runner = LISTOF(contents);
	while (runner)
	{
	    ObjPtr var;
	    real point[2];
	    object = runner -> thing;
	    MakeVar(object, BOUNDS);
	    boundsArray = GetVar(object, BOUNDS);
	    if (boundsArray && IsRealArray(boundsArray) && RANK(boundsArray) == 1 &&
		DIMS(boundsArray)[0] == 4)
	    {
		/*Valid bounds.  Change them.*/
		Array2CArray(bounds, boundsArray);

		bounds[0] = centerX + (bounds[0] + HANDLESIZE / 2 - oldCenterX) * enlargement - HANDLESIZE / 2;
		bounds[1] = centerX + (bounds[1] - HANDLESIZE / 2 - oldCenterX) * enlargement + HANDLESIZE / 2;
		bounds[2] = centerY + (bounds[2] + HANDLESIZE / 2 - oldCenterY) * enlargement - HANDLESIZE / 2;
		bounds[3] = centerY + (bounds[3] - HANDLESIZE / 2 - oldCenterY) * enlargement + HANDLESIZE / 2;

		/*We've got a new bounds, put it back*/
		boundsArray = NewRealArray(1, 4L);
		CArray2Array(boundsArray, bounds);
		SetVar(object, BOUNDS, boundsArray);
	    }

	    var = GetVar(object, STARTPOINT);
	    if (var && IsRealArray(var) && RANK(var) == 1 && DIMS(var)[0] == 2)
	    {
		Array2CArray(point, var);
		point[0] = centerX + (point[0] - oldCenterX) * enlargement;
		point[1] = centerY + (point[1] - oldCenterY) * enlargement;
		var = NewRealArray(1, 2L);
		CArray2Array(var, point);
		SetVar(object, STARTPOINT, var);
	    }
	    
	    var = GetVar(object, ENDPOINT);
	    if (var && IsRealArray(var) && RANK(var) == 1 && DIMS(var)[0] == 2)
	    {
		Array2CArray(point, var);
		point[0] = centerX + (point[0] - oldCenterX) * enlargement;
		point[1] = centerY + (point[1] - oldCenterY) * enlargement;
		var = NewRealArray(1, 2L);
		CArray2Array(var, point);
		SetVar(object, ENDPOINT, var);
	    }
	    
	    runner = runner -> next;
	}
    }
    return ObjTrue;
}

PrintCurMatrix(s)
char *s;
	{
	    Matrix haha;
	    int i, j;
	    printf("%s\n", s);
	    getmatrix(haha);
	    for (j = 0; j < 4; ++j)
	    {
		for (i = 0; i < 4; ++i)
		{
		    printf("%15g ", haha[j][i]);
		}
		printf("\n");
	    }
	    printf("\n");
	}

static ObjPtr DrawSpacePanel(object)
ObjPtr object;
/*Draws a space panel*/
{
#ifdef GRAPHICS
    int l, r, b, t;
    ObjPtr backColor, borderType;
    ObjPtr frontPanel, space;

    /*Only really draw it if we're drawing to the screen, so that
      PostScript image stuff works properly*/
    if (drawingMode != DRAW_SCREEN)
    {
	return ObjTrue;
    }

    space = GetVar(object, SPACE);
    if (space)
    {
	frontPanel = GetVar(space, FRONTPANEL);
	if (frontPanel && (frontPanel != object))
	{
	    /*I'm not the front panel*/
	    if (GetVar(frontPanel, BACKGROUND) || GetPredicate(frontPanel, TEMPOBSCURED))
	    {
		/*Front panel has a background.  I don't need to draw.*/
		return ObjTrue;
	    }
	}
    }

    Get2DIntBounds(object, &l, &r, &b, &t);
    if (IsDrawingRestricted(l, r, b, t)) return ObjFalse;

    /*Setup the new viewport*/
    StartPanel(l, r, b, t);

    /*Get the color, if any, and draw it*/
    backColor = GetVar(object, BACKGROUND);
    if (backColor)
    {
	if (GetPredicate(object, BACKNOTNEEDED))
	{
	    SetVar(object, BACKNOTNEEDED, ObjFalse);
	}
	else
	{
	    SetObjectColor(backColor);
	    FillRect(0, r - l, 0, t - b);
	    /*KLUDGE for IBM*/
	    FrameRect(0, r - l, 0, t - b);
	}
    }
    else if (GetPredicate(object, TEMPOBSCURED))
    {
	FillUIRect(0, r - l, 0, t - b, UIBLACK);
#if MACHINE == RS6000
	    /*KLUDGE for IBM*/
	    FrameRect(0, r - l, 0, t - b);
#endif
    }
    SetVar(object, BACKNOTNEEDED, ObjFalse);

    borderType = GetVar(object, BORDERTYPE);
    if (borderType)
    {
	FrameUIRect(0, r - l - 1, 0, t - b - 1, UIBLACK);
    }

    /*Draw the grid if need be*/
    if (GetPredicate(object, SHOWGRID))
    {
	real x, y;
	SetScreenGrid(object);
	SetUIColor(UIWHITE);

	for (x = GRIDX(-xGrid); x < (r - l) + xGrid; x = GRIDX(x + xGrid))
	{
	    for (y = GRIDY(-yGrid); y < (t - b) + yGrid; y = GRIDY(y + yGrid))
	    {
		FillUIRect(x - 1, x + 1, y - 1, y + 1, UIBLACK);
		FrameUIRect(x - 1, x + 1, y - 1, y + 1, UIWHITE);
	    }
	}
    }

    /*Draw the contents of the panel*/
    DrawList(GetVar(object, CONTENTS));

    if (frontPanel == object);
    {
	int spaceTool;
	ObjPtr tool;
	
	/*Get the space tool*/
	tool = GetIntVar("PressSpace", space, EDITTOOL);
	if (!tool)
	{
	    return;
	}
	spaceTool = GetInt(tool);
	if (spaceTool == ST_METER)
	{
	    /*Draw the little box*/
	}
    }

    StopPanel();
#endif

    return NULLOBJ;
}

ObjPtr ShowSpaceControls(space, windowName)
ObjPtr space;
char *windowName;
/*Makes a new control window to control space*/
{
    WinInfoPtr dialogExists, controlWindow;
    ObjPtr whichDialog;
    ObjPtr name;
    int left, right, bottom, top;

    name = GetVar(space, NAME);

    whichDialog = NewString("Controls");
    dialogExists = DialogExists((WinInfoPtr) space, whichDialog);
    if (name)
    {
	strncpy(tempStr, GetString(name), TEMPSTRSIZE);
	tempStr[TEMPSTRSIZE] = 0;
    }
    else
    {
	strcpy(tempStr, "Space");
    }
    GetTemplateBounds(SpaceTemplate, "Panel", &left, &right, &bottom, &top);
    controlWindow = GetDialog((WinInfoPtr) space, whichDialog, tempStr, 
	right - left, top - bottom, right - left, top - bottom, WINUI);

    if (!dialogExists)
    {
	ObjPtr panel, contents, checkBox;

	SetVar((ObjPtr) controlWindow, REPOBJ, space);

	/*Add controls*/
	SetVar((ObjPtr) controlWindow, HELPSTRING,
	    NewString("This window shows controls that affect attributes \
of a 3-dimensional space."));

	/*Add in a panel*/
	panel = TemplatePanel(SpaceTemplate, "Panel");
	if (!panel)
	{
	    return ObjFalse;
	}
	contents = GetVar((ObjPtr) controlWindow, CONTENTS);
	PrefixList(contents, panel);
	SetVar(panel, PARENT, (ObjPtr) controlWindow);

	contents = GetVar((ObjPtr) panel, CONTENTS);

	checkBox = TemplateCheckBox(SpaceTemplate, "Show Rotation Guides", GetPredicate(space, SHOWROTGUIDES));
	SetVar(checkBox, PARENT, panel);
	PrefixList(contents, checkBox);
	if (!GetVar(space, SHOWROTGUIDES)) SetVar(space, SHOWROTGUIDES, ObjFalse);
	AssocDirectControlWithVar(checkBox, space, SHOWROTGUIDES);
	SetVar(checkBox, HELPSTRING, NewString(
		"This controls whether rotation guides are shown in the space.  If the check box is \
on, a wire frame sphere will appear in the space.  The sphere represents the virtual \
trackball you can press and move using the middle mouse button"));


	checkBox = TemplateCheckBox(SpaceTemplate, "Show Motion Guides", GetPredicate(space, SHOWMOTGUIDES));
	SetVar(checkBox, PARENT, panel);
	PrefixList(contents, checkBox);
	if (!GetVar(space, SHOWMOTGUIDES)) SetVar(space, SHOWMOTGUIDES, ObjFalse);
	AssocDirectControlWithVar(checkBox, space, SHOWMOTGUIDES);
	SetVar(checkBox, HELPSTRING, NewString(
		"This controls whether motion guides are shown in the space.  If it is \
on, a wire frame lattice will appear within the space, aligned to space coordinates."));

    }

    return (ObjPtr) dialogExists;
}

static ObjPtr MakeSpaceAppearance(space)
ObjPtr space;
{
    SetVar(space, APPEARANCE, ObjTrue);
    ImInvalid(space);
    return ObjTrue;
}

static ObjPtr MakeSpaceTime(space)
ObjPtr space;
{
    ObjPtr clock, var;
    MakeVar(space, CLOCK);
    clock = GetVar(space, CLOCK);
    if (clock)
    {
	MakeVar(clock, TIME);
	var = GetVar(clock, TIME);
	if (var)
	{
	    SetVar(space, TIME, var);
	    return ObjTrue;
	}
    }
    SetVar(space, TIME, NewReal(0.0));
    return ObjTrue;
}

static ObjPtr ExpressObserverPlacement(track, frame)
ObjPtr track;
int frame;
/*Expresses the placement of observer placement track track at frame*/
{
    ObjPtr observer;
    Quaternion qrot;
    real location[3];
    ObjPtr var;

    /*Get the observer*/
    observer = GetObjectVar("ExpressObserverPlacement", track, REPOBJ);
    if (!observer) return ObjFalse;

    /*Find where it should be*/
    if (TweenObserverPlacement(track, frame, qrot, location))
    {
	/*Set its little variables*/
	var = NewRealArray(1, 4L);
	CArray2Array(var, qrot);
	SetVar(observer, ROTQUAT, var);
	var = NewRealArray(1, 3L);
	CArray2Array(var, location);
	SetVar(observer, LOCATION, var);
    }

    ImInvalid(observer);

    return ObjTrue;
}

static ObjPtr MakePlacementJoints(track)
ObjPtr track;
/*Makes the placement interpolateion joints for track*/
{
    ObjPtr *keyframes;		/*Array of all keyframes in track*/
    ObjPtr *joints;		/*Array of all joints in track*/
    ObjPtr var;			/*Random variable*/
    long nJoints;		/*Dimension of joints*/
    long nKeyframes;		/*Dimension of keyframes, 1+nJoints*/
    long k;			/*Counter for joints*/
    ObjPtr snap1, snap2;	/*Snapshots*/
    real weight;		/*Weight at a joint*/
    long deltaTime;		/*Delta time at a joint in frames*/
    real w1, w2;		/*Different weights*/
    real dt1, dt2;		/*Different delta times*/
    real rs1, rs2;		/*Different rotational speeds*/
    Quaternion q1, q2, qr;	/*Quaternions for calculation*/
    real axis[3];		/*Axis of a rotation*/
    real rotSpeed;		/*Rotational speed*/
    real speed;			/*Linear speed*/
    real radius;		/*Radius to pivot*/
    real distance1, distance2;	/*Distances between quaternions*/
    real pivot[3];		/*Pivot for a joint*/
    Quaternion deltaRot;	/*Delta rotation*/
    real axisLength;		/*Length of a rotation axis*/
    real loc1[3], loc2[3];	/*Locations*/
    real lloc1[3], lloc2[3];	/*Location in local (disc) coordinates*/

    /*Get the keyframes*/
    var = GetVar(track, KEYFRAMES);
    if (!var || DIMS(var)[0] < 2)
    {
	/*Don't need to make any joints*/
	SetVar(track, JOINTS, NULLOBJ);
	return ObjTrue;
    }
    keyframes = ELEMENTS(var);
    nKeyframes = DIMS(var)[0];

    /*Go through and make sure that the correct quaternions are used on
      each keyframe*/
    snap1 = GetObjectVar("MakePlacementJoints", keyframes[0], SNAPSHOT);
    if (!snap1) return NULLOBJ;
    var = GetFixedArrayVar("MakePlacementJoints", snap1, ROTQUAT, 1, 4L);
    if (!var) return NULLOBJ;
    Array2CArray(q1, var);
    for (k = 1; k < nKeyframes; ++k)
    {
	snap2 = GetObjectVar("MakePlacementJoints", keyframes[k], SNAPSHOT);
	if (!snap2) return NULLOBJ;
	var = GetFixedArrayVar("MakePlacementJoints", snap2, ROTQUAT, 1, 4L);
	if (!var) return NULLOBJ;
	Array2CArray(q2, var);

	distance1 = SQUARE(q2[0] - q1[0]) +
		    SQUARE(q2[1] - q1[1]) +
		    SQUARE(q2[2] - q1[2]) +
		    SQUARE(q2[3] - q1[3]);
	distance2 = SQUARE(-q2[0] - q1[0]) +
		    SQUARE(-q2[1] - q1[1]) +
		    SQUARE(-q2[2] - q1[2]) +
		    SQUARE(-q2[3] - q1[3]);

	if (distance2 < distance1)
	{
	    /*Have to negate second quaternion*/
	    q2[0] = -q2[0];
	    q2[1] = -q2[1];
	    q2[2] = -q2[2];
	    q2[3] = -q2[3];

	    var = NewRealArray(1, 4L);
	    CArray2Array(var, q2);
	    SetVar(snap2, ROTQUAT, var);
	}

	q1[0] = q2[0];
	q1[1] = q2[1];
	q1[2] = q2[2];
	q1[3] = q2[3];

	snap1 = snap2;
    }

    /*Create the joints*/
    nJoints = nKeyframes - 1;
    var = NewArray(AT_OBJECT, 1, &nJoints);
    joints = ELEMENTS(var);
    SetVar(track, JOINTS, var);

    /*Fill them with null joints for now*/ 
    for (k = 0; k < nJoints; ++k)
    {
	joints[k] = NewObject(jointClass, 0L);
    }

    /*Go through and calculate the delta rotation, delta time, and 
      initial weight of each joint*/
    
    snap1 = GetObjectVar("MakePlacementJoints", keyframes[0], SNAPSHOT);
    if (!snap1) return NULLOBJ;
    for (k = 0; k < nJoints; ++k)
    {
	snap2 = GetObjectVar("MakePlacementJoints", keyframes[k + 1], SNAPSHOT);
	if (!snap2) return NULLOBJ;

	var = GetArrayVar("MakePlacementJoints", snap1, ROTQUAT);
	if (!var) return NULLOBJ;
	Array2CArray(q1, var);

	var = GetArrayVar("MakePlacementJoints", snap2, ROTQUAT);
	if (!var) return NULLOBJ;
	Array2CArray(q2, var);

	/*Make delta rotation*/
	QDelta(q1, q2, qr);

	/*Make delta time*/
	var = GetIntVar("MakePlacementJoints", keyframes[k + 1], WHICHFRAME);
	if (!var) return NULLOBJ;
	deltaTime = GetInt(var);
	var = GetIntVar("MakePlacementJoints", keyframes[k], WHICHFRAME);
	if (!var) return NULLOBJ;
	deltaTime -= GetInt(var);

	/*Make weight*/
	weight = SQUARE(qr[1]) +
		 SQUARE(qr[2]) +
		 SQUARE(qr[3]);

	/*Put the delta rot, delta time, and weight in the joint*/
	var = NewRealArray(1, 4L);
	CArray2Array(var, qr);
	SetVar(joints[k], DELTAROT, var);
	SetVar(joints[k], DELTATIME, NewReal((real) deltaTime));
	SetVar(joints[k], WEIGHT, NewReal(weight));

	printf("joint %d:\n    delta = (%g, [%g %g %g])\n    weight = %g\n    dt = %ld\n",
		k,
		qr[0], qr[1], qr[2], qr[3],
		weight,
		deltaTime);

	snap1 = snap2;
    }

    /*Make consensus rotational velocity in radians per frame around an axis
      for all frames.*/

    /*First do the first frame.*/
    var = GetFixedArrayVar("MakePlacementJoints", joints[0], DELTAROT, 1, 4L);
    if (!var) return NULLOBJ;
    Array2CArray(q1, var);
    QuaternionToRot(q1, axis, &rotSpeed);
    var = GetRealVar("MakePlacementJoints", joints[0], DELTATIME);
    if (!var) return NULLOBJ;
    rotSpeed /= GetReal(var);
    var = NewRealArray(1, 3L);
    CArray2Array(var, axis);
    SetVar(keyframes[0], ROTAXIS, var);
    SetVar(keyframes[0], ROTSPEED, NewReal(rotSpeed));

    /*Now do the last frame.*/
    var = GetFixedArrayVar("MakePlacementJoints", joints[nJoints - 1], DELTAROT, 1, 4L);
    if (!var) return NULLOBJ;
    Array2CArray(q1, var);
    QuaternionToRot(q1, axis, &rotSpeed);
    var = GetRealVar("MakePlacementJoints", joints[nJoints - 1], DELTATIME);
    if (!var) return NULLOBJ;
    rotSpeed /= GetReal(var);
    var = NewRealArray(1, 3L);
    CArray2Array(var, axis);
    SetVar(keyframes[nKeyframes - 1], ROTAXIS, var);
    SetVar(keyframes[nKeyframes - 1], ROTSPEED, NewReal(rotSpeed));

    /*Now do the remainder of the frames.  Make a consensus rotational 
      velocity at each keyframe:
	1) Get the individual delta rotations from the two joints that 
	   share this keyframe.
	2) Do a weighted slerp based on the weights and time ratios
	3) Use time ratios to calculate rotational velocity.*/

    var = GetRealVar("MakePlacementJoints", joints[0], DELTATIME);
    if (!var) return NULLOBJ;
    dt1 = GetReal(var);
    var = GetFixedArrayVar("MakePlacementJoints", joints[0], DELTAROT, 1, 4L);
    if (!var) return NULLOBJ;
    Array2CArray(q1, var);
    var = GetRealVar("MakePlacementJoints", joints[0], WEIGHT);
    if (!var) return NULLOBJ;
    w1 = GetReal(var);

    for (k = 1; k < nKeyframes - 1; ++k)
    {
	var = GetRealVar("MakePlacementJoints", joints[k], DELTATIME);
	if (!var) return NULLOBJ;
	dt2 = GetReal(var);
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], DELTAROT, 1, 4L);
	if (!var) return NULLOBJ;
 	Array2CArray(q2, var);
	var = GetRealVar("MakePlacementJoints", joints[k], WEIGHT);
	if (!var) return NULLOBJ;
	w2 = GetReal(var);

	if (dt1 <= 0.0 || dt2 <= 0.0 || w1 == 0.0 || w2 == 0.0)
	{
	    /*Neither has weight, so weight evenly*/
	    w1 = w2 = dt1 = dt2 = 1.0;
	}

	/*Do the weighted averaging*/
	Slerp(q1, q2, (w2 / dt2) / (w1 / dt1 + w2 / dt2), qr);

	/*Make the rotational axis*/
	QuaternionToRot(qr, axis, &rotSpeed);
	var = NewRealArray(1, 3L);
	CArray2Array(var, axis);
	SetVar(keyframes[k], ROTAXIS, var);
	
	/*Make the speed*/
	QuaternionToRot(q1, axis, &rs1);
	rs1 /= dt1;
	QuaternionToRot(q2, axis, &rs2);
	rs2 /= dt2;
	rotSpeed = rs1 + (rs2 - rs1) * (w2 / dt2) / (w1 / dt1 + w2 / dt2);
	SetVar(keyframes[k], ROTSPEED, NewReal(rotSpeed));

	dt1 = dt2;
	q1[0] = q2[0];
	q1[1] = q2[1];
	q1[2] = q2[2];
	q1[3] = q2[3];
	w1 = w2;
    }

    /*Print out revised keyframes*/
    for (k = 0; k < nKeyframes; ++k)
    {
	printf("keyframe %d\n", k);
	var = GetVar(keyframes[k], ROTSPEED);
	printf("    speed = %g\n", GetReal(var));
	var = GetVar(keyframes[k], ROTAXIS);
	Array2CArray(axis, var);
	printf("    axis = [%g %g %g]\n", axis[0], axis[1], axis[2]);
    }

    /*Make the pivots for every joint*/
    snap1 = GetObjectVar("MakePlacementJoints", keyframes[0], SNAPSHOT);
    if (!snap1) return NULLOBJ;

    var = GetFixedArrayVar("MakePlacementJoints", snap1, LOCATION, 1, 3L);
    if (!var) return NULLOBJ;
    Array2CArray(loc1, var);

    for (k = 0; k < nJoints; ++k)
    {
	snap2 = GetObjectVar("MakePlacementJoints", keyframes[k + 1], SNAPSHOT);
	if (!snap2) return NULLOBJ;

	var = GetFixedArrayVar("MakePlacementJoints", snap2, LOCATION, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(loc2, var);

	/*Find the length of the rotation axis*/
	var = GetRealVar("MakePlacementJoints", joints[k], WEIGHT);
	if (!var) return NULLOBJ;
	weight = GetReal(var);
	axisLength = sqrt(weight);

	/*Start off with a rotation center point between the two observer points*/
	pivot[0] = (loc1[0] + loc2[0]) * 0.5;
	pivot[1] = (loc1[1] + loc2[1]) * 0.5;
	pivot[2] = (loc1[2] + loc2[2]) * 0.5;

	if (axisLength <= 0.0)
	{
	    /*There's no rotation, so arbitrarily use the center*/
	}
	else
	{
	    real t[3];		/*Translation*/
	    real r[3];		/*Radial displacement from center*/
	    real d[3];		/*Displacement vector*/
	    real rLength;	/*Length of radial direction vector*/
	    real dLength;	/*Length of displacement vector*/

	    /*Get rotation axis*/
	    var = GetFixedArrayVar("MakePlacementJoints", joints[k], DELTAROT, 1, 4L);
	    if (!var) return NULLOBJ;
	    Array2CArray(deltaRot, var);

	    /*Make normalized rotation axis direction*/
	    axis[0] = deltaRot[1] / axisLength;
	    axis[1] = deltaRot[2] / axisLength;
	    axis[2] = deltaRot[3] / axisLength;

	    /*Make translation vector*/
	    t[0] = loc2[0] - loc1[0];
	    t[1] = loc2[1] - loc1[1];
	    t[2] = loc2[2] - loc1[2];

	    /*Make a radial direction vector*/
	    CROSS(t, axis, r);

	    rLength = sqrt(SQUARE(r[0]) + SQUARE(r[1]) + SQUARE(r[2]));
	    if (rLength <= 0.0)
	    {
		/*There is no translation or it's all in the direction of
		  rotation, stick with the center of observer*/
	    }
	    else
	    {
		/*There is a translation, so we can make a stab at it*/
		real o;		/*Offset in r direction*/

		/*Normalize the r vector*/
		r[0] /= rLength;
		r[1] /= rLength;
		r[2] /= rLength;

		/*Cross a and r to get direction of displacement vector on disk*/
		CROSS(axis, r, d);

		/*Project the translation onto this vector*/
		dLength = DOT(t, d);

		/*Offset in r direction by dLength cos(theta / 2) / 
		  2 sin(theta / 2).  Lucky us, we already have
		  sin(theta / 2) in axisLength and cos(theta / 2) in the
		  scalar part of the quaternion.*/

		o = dLength * deltaRot[0] / axisLength / 2.0;

		pivot[0] += r[0] * o;
		pivot[1] += r[1] * o;
		pivot[2] += r[2] * o;

	    }
	}

	/*Stick the pivot in the joint*/
	var = NewRealArray(1, 3L);
	CArray2Array(var, pivot);
	SetVar(joints[k], PIVOT, var);

	snap1 = snap2;
	loc1[0] = loc2[0];
	loc1[1] = loc2[1];
	loc1[2] = loc2[2];
    }

    /*Print out the pivots*/
    for (k = 0; k < nJoints; ++k)
    {
	printf("Joint %d\n", k);
	var = GetVar(joints[k], PIVOT);
	Array2CArray(pivot, var);
	printf("    initial pivot = [%g %g %g]\n", pivot[0], pivot[1], pivot[2]);
    }

    /*Calculate the local location and desired velocity at the two 
	endpoints for each joint*/

    /*Get the first snapshot*/
    snap1 = GetObjectVar("MakePlacementJoints", keyframes[0], SNAPSHOT);
    if (!snap1) return NULLOBJ;

    /*Get the world location at the first snapshot*/
    var = GetFixedArrayVar("MakePlacementJoints", snap1, LOCATION, 1, 3L);
    if (!var) return NULLOBJ;
    Array2CArray(loc1, var);

    /*Get the rotation of the first snapshot*/
    var = GetFixedArrayVar("MakePlacmentJoints", snap1, ROTQUAT, 1, 4L);
    if (!var) return NULLOBJ;
    Array2CArray(q1, var);

    for (k = 0; k < nJoints; ++k)
    {
	real offset[3];			/*Offset from pivot*/
	real vl[3];			/*Component of velocity from local motion*/
	real vlt[3];			/*Transformed local velocity*/
	real vp[3];			/*Component of velocity from pivoting*/
	real vt[3];			/*Total velocity*/
	Quaternion qInv;		/*Inverse quaternion*/
	real radial[3];			/*Radial unit vector*/
	real direction[3];		/*Direction of movement due to rotation*/

	/*Get the next snapshot*/
	snap2 = GetObjectVar("MakePlacementJoints", keyframes[k + 1], SNAPSHOT);
	if (!snap2) return NULLOBJ;

	/*Get the location of the next snapshot*/
	var = GetFixedArrayVar("MakePlacementJoints", snap2, LOCATION, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(loc2, var);

	/*Get the rotation of the next snapshot*/
	var = GetFixedArrayVar("MakePlacementJoints", snap2, ROTQUAT, 1, 4L);
	if (!var) return NULLOBJ;
	Array2CArray(q2, var);

	/*Get the pivot of this joint*/
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], PIVOT, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(pivot, var);

	/*Calculate the local location for endpoint 1*/
	QInvert(q1, qInv);
	offset[0] = loc1[0] - pivot[0];
	offset[1] = loc1[1] - pivot[1];
	offset[2] = loc1[2] - pivot[2];
	QRot(offset, qInv, lloc1);
	var = NewRealArray(1, 3L);
	CArray2Array(var, lloc1);
	SetVar(joints[k], LLOCATION1, var);

	/*Calculate the local location for endpoint 2*/
	QInvert(q2, qInv);
	offset[0] = loc2[0] - pivot[0];
	offset[1] = loc2[1] - pivot[1];
	offset[2] = loc2[2] - pivot[2];
	QRot(offset, qInv, lloc2);
	var = NewRealArray(1, 3L);
	CArray2Array(var, lloc2);
	SetVar(joints[k], LLOCATION2, var);

	/*Get the delta time for this joint*/
	var = GetRealVar("MakePlacementJoints", joints[k], DELTATIME);
	if (!var) return NULLOBJ;
	deltaTime = GetReal(var);

	/*Calculate the velocity component due to motion within local coordinates*/
	vl[0] = (lloc2[0] - lloc1[0]) / deltaTime;
	vl[1] = (lloc2[1] - lloc1[1]) / deltaTime;
	vl[2] = (lloc2[2] - lloc1[2]) / deltaTime;
	
	/*Calculate the velocity component due to rotation at endpoint 1*/

	/*Get rotational speed*/
	var = GetRealVar("MakePlacementJoints", keyframes[k], ROTSPEED);
	if (!var) return NULLOBJ;
	rotSpeed = GetReal(var);

	/*Calculate the radius*/
	radius = rsqrt(SQUARE(lloc1[0]) + SQUARE(lloc1[1]) + SQUARE(lloc1[2]));

	if (rotSpeed == 0.0 || radius <= 0.0)
	{
	    /*No contributed velocity due to rotation*/
	   vp[0] = vp[1] = vp[2] = 0.0;
	}
	else
	{
	    /*Calculate the speed*/
	    speed = radius * rotSpeed;

	    /*Get the rotational axis*/
	    var = GetFixedArrayVar("MakePlacementJoints", keyframes[k], ROTAXIS, 1, 3L);
	    if (!var) return NULLOBJ;
	    Array2CArray(axis, var);

	    /*Calculate the radial and movement direction*/
	    radial[0] = loc1[0] - pivot[0];
	    radial[1] = loc1[1] - pivot[1];
	    radial[2] = loc1[2] - pivot[2];
	    NORMALIZE(radial);
	    CROSS(radial, axis, direction);
	    /*Direction is not normalized, so it compensates for skew radial*/

	    /*Now calculate the component*/
	    vp[0] = direction[0] * speed;
	    vp[1] = direction[1] * speed;
	    vp[2] = direction[2] * speed;
	}

	/*Rotate the linear velocity to get it in world coordinates*/
	QRot(vl, q1, vlt);

	/*Make total velocity*/
	vt[0] = vp[0] + vlt[0]; 
	vt[1] = vp[1] + vlt[1]; 
	vt[2] = vp[2] + vlt[2];

	/*Make it the desired velocity*/
	var = NewRealArray(1, 3L);
	CArray2Array(var, vt);
	SetVar(joints[k], DESVEL1, var);

	/*Calculate the velocity component due to rotation at endpoint 2*/

	/*Get rotational speed*/
	var = GetRealVar("MakePlacementJoints", keyframes[k], ROTSPEED);
	if (!var) return NULLOBJ;
	rotSpeed = GetReal(var);

	/*Calculate the radius*/
	radius = rsqrt(SQUARE(lloc2[0]) + SQUARE(lloc2[1]) + SQUARE(lloc2[2]));

	if (rotSpeed == 0.0 || radius <= 0.0)
	{
	    /*No contributed velocity due to rotation*/
	   vp[0] = vp[1] = vp[2] = 0.0;
	}
	else
	{
	    /*Calculate the speed*/
	    speed = radius * rotSpeed;

	    /*Get the rotational axis*/
	    var = GetFixedArrayVar("MakePlacementJoints", keyframes[k], ROTAXIS, 1, 3L);
	    if (!var) return NULLOBJ;
	    Array2CArray(axis, var);

	    /*Calculate the radial and movement direction*/
	    radial[0] = loc2[0] - pivot[0];
	    radial[1] = loc2[1] - pivot[1];
	    radial[2] = loc2[2] - pivot[2];
	    NORMALIZE(radial);
	    CROSS(radial, axis, direction);

	    /*Now calculate the component*/
	    vp[0] = direction[0] * speed;
	    vp[1] = direction[1] * speed;
	    vp[2] = direction[2] * speed;
	}

	/*Rotate the linear velocity to get it in world coordinates*/
	QRot(vl, q2, vlt);

	/*Make total velocity*/
	vt[0] = vp[0] + vlt[0]; 
	vt[1] = vp[1] + vlt[1]; 
	vt[2] = vp[2] + vlt[2];

	/*Make it the desired velocity*/
	var = NewRealArray(1, 3L);
	CArray2Array(var, vt);
	SetVar(joints[k], DESVEL2, var);

	/*Shift the variables down for the next step*/
	snap1 = snap2;
	loc1[0] = loc2[0];
	loc1[1] = loc2[1];
	loc1[2] = loc2[2];
	q1[0] = q2[0];
	q1[1] = q2[1];
	q1[2] = q2[2];
	q1[3] = q2[3];
    }

    /*Use the desired velocities to make consensus velocities for all keyframes*/
    SetVar(keyframes[0], WORLDVELOCITY, GetVar(joints[0], DESVEL1));
    SetVar(keyframes[nKeyframes - 1], WORLDVELOCITY, GetVar(joints[nJoints - 1], DESVEL2));

    var = GetRealVar("MakePlacementJoints", joints[0], WEIGHT);
    if (!var) return NULLOBJ;
    w1 = GetReal(var);

    for (k = 1; k < nKeyframes - 1; ++k)
    {
	real desVel1[3], desVel2[3];	/*Desired velocities*/
	real wv[3];			/*Final world velocity*/

	var = GetRealVar("MakePlacementJoints", joints[k], WEIGHT);
	if (!var) return NULLOBJ;
	w2 = GetReal(var);

	if (w1 + w2 <= 0.0)
	{
	    /*Neither has weight, so weigh evenly*/
	}

	var = GetFixedArrayVar("MakePlacementJoints", joints[k - 1], DESVEL2, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(desVel1, var);

	var = GetFixedArrayVar("MakePlacementJoints", joints[k], DESVEL1, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(desVel2, var);

	wv[0] = (desVel1[0] * w1 + desVel2[0] * w2) / (w1 + w2);
	wv[1] = (desVel1[1] * w1 + desVel2[1] * w2) / (w1 + w2);
	wv[2] = (desVel1[2] * w1 + desVel2[2] * w2) / (w1 + w2);

	var = NewRealArray(1, 3L);
	CArray2Array(var, wv);
	SetVar(keyframes[k], WORLDVELOCITY, var);

	w1 = w2;
    }

    /*Print out the world velocities*/
    for (k = 0; k < nKeyframes; ++k)
    {
	real wv[3];
	printf("keyframe %d\n", k);
	var = GetVar(keyframes[k], WORLDVELOCITY);
	Array2CArray(wv, var);
	printf("    world velocity = %g %g %g\n", wv[0], wv[1], wv[2]);
    }

    /*Use the consensus world velocity and rotational velocities to work 
	backward to get individual endpoint local linear velocities*/

    snap1 = GetObjectVar("MakePlacementJoints", keyframes[0], SNAPSHOT);
    if (!snap1) return NULLOBJ;

    var = GetFixedArrayVar("MakePlacementJoints", snap1, LOCATION, 1, 3L);
    if (!var) return NULLOBJ;
    Array2CArray(loc1, var);

    var = GetFixedArrayVar("MakePlacementJoints", snap1, ROTQUAT, 1, 4L);
    if (!var) return NULLOBJ;
    Array2CArray(q1, var);

    for (k = 0; k < nJoints; ++k)
    {
	real vp[3];		/*Component due to pivot*/
	real direction[3];	/*Direction of pivot motion*/
	real radial[3];		/*Radial vector*/
	real vl[3];		/*Local velocity*/
	real vlt[3];		/*Local velocity transformed*/
	real wv[3];		/*World velocity*/
	Quaternion qInv;	/*Inverse quaternion*/

	/*Get pivot*/
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], PIVOT, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(pivot, var);

	/*Do endpoint1*/

	/*Get axis and speed of endpoint 1*/
	var = GetFixedArrayVar("MakePlacementJoints", keyframes[k], ROTAXIS, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(axis, var);
	var = GetRealVar("MakePlacementJoints", keyframes[k], ROTSPEED);
	if (!var) return NULLOBJ;
	rotSpeed = GetReal(var);

	/*Get local location*/
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], LLOCATION1, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(lloc1, var);

	/*Calculate the radius*/
	radius = sqrt(SQUARE(lloc1[0]) + SQUARE(lloc1[1]) + SQUARE(lloc1[2]));

	if (radius <= 0.0 || rotSpeed == 0.0)
	{
	    /*No rotational component*/
	    vp[0] = vp[1] = vp[2] = 0.0;
	}
	else
	{
	    /*Now the total speed, not compensated by skew radial*/
	    speed = radius * rotSpeed;

	    /*Calculate the radial and movement direction*/
	    radial[0] = loc1[0] - pivot[0];
	    radial[1] = loc1[1] - pivot[1];
	    radial[2] = loc1[2] - pivot[2];
	    NORMALIZE(radial);
	    CROSS(radial, axis, direction);
	    /*Direction is not normalized, so it compensates for skew radial*/

	    /*Now calculate the component*/
	    vp[0] = direction[0] * speed;
	    vp[1] = direction[1] * speed;
	    vp[2] = direction[2] * speed;
	}

	/*Get world velocity*/
	var = GetFixedArrayVar("MakePlacementJoints", keyframes[k], WORLDVELOCITY, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(wv, var);

	/*Get linear component of velocity*/
	vlt[0] = wv[0] - vp[0];
	vlt[1] = wv[1] - vp[1];
	vlt[2] = wv[2] - vp[2];

	/*Rotate back to local coordinates*/
	QInvert(q1, qInv);
	QRot(vlt, qInv, vl);

	/*Put vl back as local velocity*/
	var = NewRealArray(1, 3L);
	CArray2Array(var, vl);
	SetVar(joints[k], LVELOCITY1, var);

	/*Go to next endpoint*/

	snap2 = GetObjectVar("MakePlacementJoints", keyframes[k + 1], SNAPSHOT);
	if (!snap2) return NULLOBJ;

	var = GetFixedArrayVar("MakePlacementJoints", snap2, LOCATION, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(loc2, var);

	var = GetFixedArrayVar("MakePlacementJoints", snap2, ROTQUAT, 1, 4L);
	if (!var) return NULLOBJ;
	Array2CArray(q2, var);

	/*Do endpoint 2*/

	/*Get axis and speed of endpoint 2*/
	var = GetFixedArrayVar("MakePlacementJoints", keyframes[k + 1], ROTAXIS, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(axis, var);
	var = GetRealVar("MakePlacementJoints", keyframes[k + 1], ROTSPEED);
	if (!var) return NULLOBJ;
	rotSpeed = GetReal(var);

	/*Get local location*/
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], LLOCATION2, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(lloc2, var);

	/*Calculate the radius*/
	radius = sqrt(SQUARE(lloc2[0]) + SQUARE(lloc2[1]) + SQUARE(lloc2[2]));

	if (radius <= 0.0 || rotSpeed == 0.0)
	{
	    /*No rotational component*/
	    vp[0] = vp[1] = vp[2] = 0.0;
	}
	else
	{
	    /*Now the total speed, not compensated by skew radial*/
	    speed = radius * rotSpeed;

	    /*Calculate the radial and movement direction*/
	    radial[0] = loc2[0] - pivot[0];
	    radial[1] = loc2[1] - pivot[1];
	    radial[2] = loc2[2] - pivot[2];
	    NORMALIZE(radial);
	    CROSS(radial, axis, direction);
	    /*Direction is not normalized, so it compensates for skew radial*/

	    /*Now calculate the component*/
	    vp[0] = direction[0] * speed;
	    vp[1] = direction[1] * speed;
	    vp[2] = direction[2] * speed;
	}

	/*Get world velocity*/
	var = GetFixedArrayVar("MakePlacementJoints", keyframes[k + 1], WORLDVELOCITY, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(wv, var);

	/*Get linear component of velocity*/
	vlt[0] = wv[0] - vp[0];
	vlt[1] = wv[1] - vp[1];
	vlt[2] = wv[2] - vp[2];

	/*Rotate back to local coordinates*/
	QInvert(q2, qInv);
	QRot(vlt, qInv, vl);

	/*Put vl back as local velocity*/
	var = NewRealArray(1, 3L);
	CArray2Array(var, vl);
	SetVar(joints[k], LVELOCITY2, var);

	/*Shift variables for next iteration*/
	snap1 = snap2;
	loc1[0] = loc2[0];
	loc1[1] = loc2[1];
	loc1[2] = loc2[2];
	q1[0] = q2[0];
	q1[1] = q2[1];
	q1[2] = q2[2];
	q1[3] = q2[3];
    }

    /*Print out local velocities*/
    for (k = 0; k < nJoints; ++k)
    {
	real vl[3];
	printf("joint %d:\n", k);
	var = GetVar(joints[k], LVELOCITY1);
	Array2CArray(vl, var);
	printf("    vel1 = %g %g %g\n", vl[0], vl[1], vl[2]);
	var = GetVar(joints[k], LVELOCITY2);
	Array2CArray(vl, var);
	printf("    vel2 = %g %g %g\n", vl[0], vl[1], vl[2]);
    }

    /*Go through and make A and B points for Bezier rotations*/
    for (k = 0; k < nKeyframes; ++k)
    {
	Quaternion qA, qB, qInv;
	real tempRotation;
	int thisFrame;
	real dt;

	snap1 = GetObjectVar("MakePlacementJoints", keyframes[k], SNAPSHOT);
	if (!snap1) return NULLOBJ;

	/*Get axis of rotation*/
	var = GetFixedArrayVar("MakePlacementJoints", keyframes[k], ROTAXIS, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(axis, var);

	/*Get speed of rotation in frame units*/
	var = GetRealVar("MakePlacementJoints", keyframes[k], ROTSPEED);
	if (!var) return NULLOBJ;
	rotSpeed = GetReal(var);

	/*Get quaternion at this keyframe*/
	var = GetFixedArrayVar("MakePlacementJoints", snap1, ROTQUAT, 1, 4L);
	if (!var) return NULLOBJ;
	Array2CArray(q1, var);

	/*Get frame number at this keyframe*/

	if (k < nKeyframes - 1)
	{
	    /*There's an A.*/

	    /*Get delta time to next joint*/
	    var = GetRealVar("MakePlacementJoints", joints[k], DELTATIME);
	    if (!var) return NULLOBJ;
	    dt = GetReal(var);

	    /*Make and adjusted rotation from speed and delta time,
	      divided by 3 for Bezier stuff*/

	    tempRotation = rotSpeed * dt / 3.0;
	    RotToQuaternion(axis, tempRotation, q2);

	    /*Produce A quaternion*/
	    QMult(q1, q2, qr);
	    var = NewRealArray(1, 4L);
	    CArray2Array(var, qr);
	    SetVar(keyframes[k], ROTA, var);
	}
	if (k > 0)
	{
	    /*There's a B.*/

	    /*Get delta time to next joint*/
	    var = GetRealVar("MakePlacementJoints", joints[k - 1], DELTATIME);
	    if (!var) return NULLOBJ;
	    dt = GetReal(var);

	    /*Make and adjusted rotation from speed and delta time,
	      divided by 3 for Bezier stuff*/

	    tempRotation = -rotSpeed * dt / 3.0;
	    RotToQuaternion(axis, tempRotation, q2);

	    /*Produce A quaternion*/
	    QMult(q1, q2, qr);
	    var = NewRealArray(1, 4L);
	    CArray2Array(var, qr);
	    SetVar(keyframes[k], ROTB, var);
	}
    }

    /*Now go through and make A and B in local coords for joint endpoints*/
    for (k = 0; k < nJoints; ++k)
    {
	real lv1[3], lv2[3];		/*Local velocities*/
	real cp[3];			/*Control point*/
	real dt;			/*Delta time*/

	/*Get delta time*/
	var = GetRealVar("MakePlacementJoints", joints[k], DELTATIME);
	if (!var) return NULLOBJ;
	dt = GetReal(var);

	/*Get local locations*/
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], LLOCATION1, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(lloc1, var);

	var = GetFixedArrayVar("MakePlacementJoints", joints[k], LLOCATION2, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(lloc2, var);

	/*Get local velocities*/
	var = GetFixedArrayVar("MakePlacementJoints", joints[k], LVELOCITY1, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(lv1, var);

	var = GetFixedArrayVar("MakePlacementJoints", joints[k], LVELOCITY2, 1, 3L);
	if (!var) return NULLOBJ;
	Array2CArray(lv2, var);

	/*Make A for endpoint 1*/
	cp[0] = lloc1[0] + lv1[0] * dt / 3.0;
	cp[1] = lloc1[1] + lv1[1] * dt / 3.0;
	cp[2] = lloc1[2] + lv1[2] * dt / 3.0;
	var = NewRealArray(1, 3L);
	CArray2Array(var, cp);
	SetVar(joints[k], LOCA, var);

	/*Make B for endpoint 2*/
	cp[0] = lloc2[0] - lv2[0] * dt / 3.0;
	cp[1] = lloc2[1] - lv2[1] * dt / 3.0;
	cp[2] = lloc2[2] - lv2[2] * dt / 3.0;
	var = NewRealArray(1, 3L);
	CArray2Array(var, cp);
	SetVar(joints[k], LOCB, var);

    }

    return ObjTrue;
}

#ifdef HAVE_PROTOTYPES
Bool TweenObserverPlacement(ObjPtr track, int whichFrame, Quaternion qr, real locr[3])
#else
Bool TweenObserverPlacement(track, whichFrame, qr, locr)
ObjPtr track;
int whichFrame;
Quaternion qr;
real locr[3];
#endif
/*Finds a tween for the observer on track at whichFrame, returning the value
  in qr and locr*/
{
    int k;			/*Index to which joint this is*/
    ObjPtr var;			/*Random variable*/
    ObjPtr *keyframes, *joints;	/*Keyframes and joints*/
    ObjPtr snap1, snap2;	/*Snapshots*/
    real s;			/*Parameter into quaternion*/
    int frame1;			/*WHICHFRAME of the two frames*/
    int frame2;
    Quaternion q1, q2;		/*First and second quaternions*/
    Quaternion qa, qb;		/*A and B quaternions*/
    Quaternion qh0, qh1, qh2;	/*Bezier interpolation quaternions*/
    real l1[3], l2[3];		/*First and second locations*/
    real la[3], lb[3];		/*A and B locations*/
    real lh0[3], lh1[3], lh2[3];/*Bezier interpolation locations*/
    real lr[3];			/*Local result location*/
    real offset[3];		/*Offset from pivot to result*/
    real pivot[3];		/*Pivot*/

    /*Get the keyframes*/
    var = GetVar(track, KEYFRAMES);
    if (!var) return false;
    keyframes = ELEMENTS(var);

    /*Find which joint to use*/
    k = SearchIntVar(var, WHICHFRAME, whichFrame);
    if (k > 0) --k;
    if (k >= DIMS(var)[0] - 1) --k;
    if (k < 0) return false;

    /*Get the joints*/
    var = GetVar(track, JOINTS);
    if (!var) return false;
    joints = ELEMENTS(var);

    var = GetIntVar("TweenObserverPlacement", keyframes[k], WHICHFRAME);
    if (!var) return false;
    frame1 = GetInt(var);

    var = GetIntVar("TweenObserverPlacement", keyframes[k + 1], WHICHFRAME);
    if (!var) return false;
    frame2 = GetInt(var);

    /*Make the parameter in the range [0...1]*/
    s = ((real) (whichFrame - frame1)) / ((real) (frame2 - frame1));

    /*Get the snapshots*/
    snap1 = GetObjectVar("TweenObserverPlacement", keyframes[k], SNAPSHOT);
    if (!snap1) return false;
    snap2 = GetObjectVar("TweenObserverPlacement", keyframes[k + 1], SNAPSHOT);
    if (!snap2) return false;

    /*Interpolate quaternions*/

    /*Get the source quaternions*/
    var = GetFixedArrayVar("TweenObserverPlacement", snap1, ROTQUAT, 1, 4L);
    if (!var) return false;
    Array2CArray(q1, var);

    var = GetFixedArrayVar("TweenObserverPlacement", snap2, ROTQUAT, 1, 4L);
    if (!var) return false;
    Array2CArray(q2, var);

    /*Get the A and B quaternions*/
    var = GetFixedArrayVar("TweenObserverPlacement", keyframes[k], ROTA, 1, 4L);
    if (!var) return false;
    Array2CArray(qa, var);

    var = GetFixedArrayVar("TweenObserverPlacement", keyframes[k + 1], ROTB, 1, 4L);
    if (!var) return false;
    Array2CArray(qb, var);

    /*Calculate 3 helper quaternions from q1, qa, qb, and q2*/
    Slerp(q1, qa, s, qh0);
    Slerp(qa, qb, s, qh1);
    Slerp(qb, q2, s, qh2);

    /*Interpolate qh0, qh1, and qh2 to get qh0 and qh1*/
    Slerp(qh0, qh1, s, qh0);
    Slerp(qh1, qh2, s, qh1);

    /*Finally, interpolate qh0 and qh1 to get the result*/
    Slerp(qh0, qh1, s, qr);

    /*Interpolate locations*/

    /*Get the source locations*/
    var = GetFixedArrayVar("TweenObserverPlacement", joints[k], LLOCATION1, 1, 3L);
    if (!var) return false;
    Array2CArray(l1, var);

    var = GetFixedArrayVar("TweenObserverPlacement", joints[k], LLOCATION2, 1, 3L);
    if (!var) return false;
    Array2CArray(l2, var); 

    /*Get the A and B locations*/
    var = GetFixedArrayVar("TweenObserverPlacement", joints[k], LOCA, 1, 3L);
    if (!var) return false;
    Array2CArray(la, var);

    var = GetFixedArrayVar("TweenObserverPlacement", joints[k], LOCB, 1, 3L);
    if (!var) return false;
    Array2CArray(lb, var);

    /*Calculate 3 helper locations from l1, la, lb, and l2*/
    INT3(l1, la, s, lh0);
    INT3(la, lb, s, lh1);
    INT3(lb, l2, s, lh2);

    /*Interpolate lh0, lh1, and lh2 to get lh0 and lh1*/
    INT3(lh0, lh1, s, lh0);
    INT3(lh1, lh2, s, lh1);

    /*Finally, interpolate lh0 and lh1 to get result*/
    INT3(lh0, lh1, s, lr);

    /*Rotate local position by quaternion to get offset*/
    QRot(lr, qr, offset);

    /*Offset pivot to get result*/
    var = GetFixedArrayVar("TweenObserverPlacement", joints[k], PIVOT, 1, 3L);
    if (!var) return false;
    Array2CArray(pivot, var);

    locr[0] = pivot[0] + offset[0];
    locr[1] = pivot[1] + offset[1];
    locr[2] = pivot[2] + offset[2];

    return true;
}

void InitSpaces()
/*Initializes the spaces*/
{
    ObjPtr icon;
    ObjPtr list;
    ObjPtr var;
    real curViewClip[2];
 
    /*Create a class of spaces*/
    spaceClass = NewObject(NULLOBJ, 0);
    AddToReferenceList(spaceClass);
    SetVar(spaceClass, CLASSID, NewInt(CLASS_SPACE));
    SetMethod(spaceClass, DRAW, DrawSpace);
    SetVar(spaceClass, NAME, NewString("Space"));
    SetMethod(spaceClass, PRESS, PressSpace);
    SetMethod(spaceClass, TURNDIAL, TurnDialSpace);
    SetMethod(spaceClass, KEYDOWN, KeyDownSpace);
    SetMethod(spaceClass, BOUNDSINVALID, SpaceBoundsInvalid);
    SetVar(spaceClass, TYPESTRING, NewString("space"));
    SetVar(spaceClass, HELPSTRING,
	NewString("A space provides a 3-dimensional world for visualization \
objects.  Objects are automatically scaled and translated to appear near the \
center by default.  Any number of visualization objects can exist within a space. \
Spaces are controlled by controllers, such as observers and lights.\n\
\n\
The tools control panel on the left controls how the mouse affects the space.  \
For more information on a particular tool, use help in context on the tool button."));

#ifdef GRAPHICS
    lmdef(DEFMATERIAL, 1, 0, NULL);
#endif

    SetMethod(spaceClass, FORALLOBJECTS, ForAllSpaceObjects); 
    SetMethod(spaceClass, NEWCTLWINDOW, ShowSpaceControls);
    SetMethod(spaceClass, SHOWCONTROLS, NewControlWindow);
    DeclareDependency(spaceClass, TIME, CLOCK);
    DeclareIndirectDependency(spaceClass, TIME, CLOCK, TIME);
    SetMethod(spaceClass, TIME, MakeSpaceTime);
    DeclareDependency(spaceClass, APPEARANCE, TIME);
    SetMethod(spaceClass, APPEARANCE, MakeSpaceAppearance);

    DeclareDependency(spaceClass, APPEARANCE, RENDERER);

    DeclareDependency(spaceClass, APPEARANCE, RENDERTYPE);
    DeclareDependency(spaceClass, RENDERTYPE, RENDERER);
    DeclareIndirectDependency(spaceClass, RENDERTYPE, RENDERER, RENDERTYPE);
    SetMethod(spaceClass, RENDERTYPE, MakeSpaceRenderType);

    DeclareDependency(spaceClass, APPEARANCE, FILTERTYPE);
    DeclareDependency(spaceClass, FILTERTYPE, RENDERER);
    DeclareIndirectDependency(spaceClass, FILTERTYPE, RENDERER, FILTERTYPE);
    SetMethod(spaceClass, FILTERTYPE, MakeSpaceFilterType);

    /*Initialize a space panel*/
    spacePanelClass = NewObject(panelClass, 0);
    AddToReferenceList(spacePanelClass);
    SetMethod(spacePanelClass, DROPOBJECTS, DropInSpacePanel);
    SetMethod(spacePanelClass, RESHAPE, ReshapeSpacePanel);
    SetMethod(spacePanelClass, DRAW, DrawSpacePanel);
    SetMethod(spacePanelClass, BOUNDSINVALID, SpacePanelBoundsInvalid);

    /*Initialize a space back panel*/
    spaceBackPanelClass = NewObject(spacePanelClass, 0);
    AddToReferenceList(spaceBackPanelClass);
    SetVar(spaceBackPanelClass, BACKGROUND, NewInt(UIBLACK));

    /*Create class of space controllers*/
    controllerClass = NewObject(NULLOBJ, 0);
    AddToReferenceList(controllerClass);
    SetVar(controllerClass, CONTROLLERP, ObjTrue);
    SetMethod(controllerClass, SHOWCONTROLS, NewControlWindow);

    /*Create class of clocks*/
    clockClass = NewObject(controllerClass, 0);
    AddToReferenceList(clockClass);
    SetMethod(clockClass, MARKTIME, MarkClockTime);
    SetMethod(clockClass, CLONE, CloneClock);
    SetMethod(clockClass, BINDTOSPACE, BindClockToSpace);
    SetMethod(clockClass, TIMEBOUNDS, MakeClockTimeBounds);
    SetMethod(clockClass, NEWCTLWINDOW, ShowClockControls);
    SetVar(clockClass, DOUBLECLICK, NewString(OF_SHOW_CONTROLS));
    SetMethod(clockClass, REINIT, MakeClockDatasets);
    SetVar(clockClass, CLASSID, NewInt(CLASS_CLOCK));
    DeclareDependency(clockClass, TIMESTEPS, DATASETS);
    SetVar(clockClass, DTIMEPER, NewReal(1));
    SetMethod(clockClass, TIMESTEPS, MakeClockTimeSteps);
    SetMethod(clockClass, DTIME, MakeClockDTime);
    SetVar(clockClass, ONEONLY, ObjTrue);
    SetVar(clockClass, DEFAULTICON, icon = NewIcon(0, 0, ICONCLOCK, "Clock"));
    SetVar(icon, HELPSTRING,
	NewString("This icon represents a clock.  The clock controls the current \
time displayed within a space and the rate at which time goes forward or backward.  \
You can see controls for this clock by selecting it and choosing the Show Controls \
item in the Object menu.\n\
\n\
You can drag this icon into the icon corral of another visualization window to have \
it control the other space as well.  \
A space can only be controlled by one clock at a time.  If you drag another \
clock icon into this space, this clock will be replaced.\n\
\n\
You can place a time display in the image of the space itself by dragging this \
icon into the space itself."));
    SetVar(clockClass, NAME, NewString("Clock"));
    DeclareDependency(clockClass, TIMEFORMAT, DATASETS);
    SetMethod(clockClass, TIMEFORMAT, MakeClockTimeFormat);

    /*Creat class of worlds*/
    worldClass = NewObject(controllerClass, 0L);
    AddToReferenceList(worldClass);
    icon = NewIcon(0, 0, ICONWORLD, "World");
    SetVar(worldClass, CONTROLICON, icon);
    SetVar(icon, PANELHELP, NewString("This control panel allows you to edit \
parameters that control the relationship between the world coordinate system and \
the space coordinates in which the visualization objects reside."));
    SetMethod(worldClass, ADDCONTROLS, AddWorldControls);

    /*Create class of placements*/
    placementClass = NewObject(worldClass, 0L);
    AddToReferenceList(placementClass);
    AddSnapVar(placementClass, LOCATION);
    AddSnapVar(placementClass, FORWARDVECTOR);
    AddSnapVar(placementClass, UPVECTOR);
    AddSnapVar(placementClass, FOCUSDIST);
    icon = NewIcon(0, 0, ICONPLACEMENT, "Placement");
    SetVar(placementClass, CONTROLICON, icon);
    SetVar(icon, PANELHELP, NewString("This control panel allows you to edit the \
numerical parameters that define the observer.  You can edit parameters in either \
observer coordinates (normalized) or space coordinates (values appropriate for the datasets)."));
    DeclareDependency(placementClass, FORWARDVECTOR, ROTQUAT);
    SetMethod(placementClass, FORWARDVECTOR, MakeObserverForwardVector);
    DeclareDependency(placementClass, UPVECTOR, ROTQUAT);
    SetMethod(placementClass, ADDCONTROLS, AddPlacementControls);
    SetMethod(placementClass, UPVECTOR, MakeObserverUpVector);
    {
	/*Give it an initial identity rotation quaternion*/
	ObjPtr var;
	Quaternion rotQuat;

	rotQuat[0] = 1.0;
	rotQuat[1] = 0.0;
	rotQuat[2] = 0.0;
	rotQuat[3] = 0.0;
	var = NewRealArray(1, 4L);
	CArray2Array(var, rotQuat);
	SetVar(placementClass, ROTQUAT, var);
    }

    /*Create a class of observers with stereo*/
    stereoClass = NewObject(placementClass, 0L);
    AddToReferenceList(stereoClass);
    icon = NewIcon(0, 0, ICONGLASSES, "Stereo");
    SetVar(stereoClass, CONTROLICON, icon);
    SetVar(icon, PANELHELP, NewString("This panel shows controls for stereo \
viewing of the visualization.  Most of the techniques require special pieces of \
equipment, the least expensive is a pair of Red/Cyan glasses which costs less than \
fifty cents."));
    SetMethod(stereoClass, ADDCONTROLS, AddStereoControls);
 
    /*Create class of observers*/
    observerClass = NewObject(stereoClass, 0L);
    AddToReferenceList(observerClass);
    icon = NewIcon(0, 0, ICONOBSERVER, "View");
    SetVar(observerClass, CONTROLICON, icon);
    SetVar(icon, PANELHELP, NewString("This panel shows visual controls for changing \
the parameters of an observer that are not easily changed by clicking and \
dragging in the space."));
    SetMethod(observerClass, BINDTOSPACE, BindObserverToSpace);
    SetMethod(observerClass, NEWCTLWINDOW, ShowObserverControls);
    SetVar(observerClass, DOUBLECLICK, NewString(OF_SHOW_CONTROLS));
    SetMethod(observerClass, LOCALCOPY, MakeLocalCopy);
    SetMethod(observerClass, ADDCONTROLS, AddViewControls);
    SetVar(observerClass, VIEWTYPE, NewInt(VT_PERSPECTIVE));
    SetVar(observerClass, STEREOMODE, NewInt(SM_MONO));
    if (windowSystem == WS_CAVESIM)
    {
	SetVar(observerClass, FOCUSDIST, NewReal(0.1));
    }
    else
    {
	SetVar(observerClass, FOCUSDIST, NewReal(INITEYEDIST));
    }
    SetVar(observerClass, VIEWFIELD, NewReal(INITAOV));
    SetMethod(observerClass, DRAW, DrawObserver);
    curViewClip[0] = INITNEARCLIP;
    curViewClip[1] = INITFARCLIP;
    var = NewRealArray(1, 2L);
    CArray2Array(var, curViewClip);
    SetVar(observerClass, VIEWCLIP, var);
    {
	ObjPtr clipArray;
	real clip[2];
	clip[0] = INITNEARCLIP;
	clip[1] = INITFARCLIP;
	clipArray = NewRealArray(1, 2L);
	CArray2Array(clipArray, clip);
    }
    {
	ObjPtr anArray;
	real posn[3];
	posn[0] = 0.0;
	posn[1] = 0.0;
	if (windowSystem == WS_CAVESIM)
	{
	    posn[2] = 0.1;
	}
	else
	{
	    posn[2] = INITEYEDIST;
	}
	posn[2] = INITEYEDIST;
	anArray = NewRealArray(1, 3L);
	CArray2Array(anArray, posn);
	SetVar(observerClass, LOCATION, anArray);
	posn[0] = 0.0;
	posn[1] = 0.0;
	posn[2] = -1.0;
	anArray = NewRealArray(1, 3L);
	CArray2Array(anArray, posn);
	SetVar(observerClass, FORWARDVECTOR, anArray);
	posn[0] = 0.0;
	posn[1] = 1.0;
	posn[2] = 0.0;
	anArray = NewRealArray(1, 3L);
	CArray2Array(anArray, posn);
	SetVar(observerClass, UPVECTOR, anArray);
	posn[0] = 0.0;
	posn[1] = 0.0;
	posn[2] = 0.0;
	anArray = NewRealArray(1, 3L);
	CArray2Array(anArray, posn);
	SetVar(observerClass, SPACEORIGIN, anArray);
	SetVar(observerClass, SPACESCALE, NewReal(1.0));
    }
    SetVar(observerClass, FOCUSDIST, NewReal((real) INITEYEDIST));

    SetMethod(observerClass, MARKTIME, WakeObserver);
    icon = NewIcon(0, 0, ICONOBSERVER, "Observer");
    SetVar(icon, HELPSTRING, 
	NewString("This icon represents an observer.  The observer represents \
you looking into the 3-dimensional space.  You can change attributes such as the \
viewing angle and near and far clipping planes in the control panel, which you \
can show by selecting the icon and choosing the Show Controls \
item in the Object menu.\n\
\n\
You can drag this icon into the icon corral of another visualization window to have \
it control the other space as well.  When an observer controls more than one \
space, the view of the objects in the spaces are tied together.  This is very \
useful for viewing several similar datasets from the same viewpoint at once.  \
A space can only be controlled by one observer at a time.  If you drag another \
observer icon into this space, this observer will be replaced."));
#if 0
    SetMethod(icon, ICONEXTRADRAW, DrawExtraObserverIcon);
#endif
    SetVar(observerClass, DEFAULTICON, icon);
    SetVar(observerClass, CLASSID, NewInt(CLASS_OBSERVER));
    SetVar(observerClass, ONEONLY, ObjTrue);
    SetVar(observerClass, NAME, NewString("Observer"));
    SetVar(observerClass, AUTOADJUST, NewInt(1));
    SetVar(observerClass, USESPACECOORDS, NewInt(0));
    SetMethod(observerClass, CLONE, CloneObserver);
    SetVar(observerClass, BINOCULARITY, NewReal(0.4));

    /*Create class of renderers*/
    rendererClass = NewObject(controllerClass, 0);
    AddToReferenceList(rendererClass);
    SetMethod(rendererClass, BINDTOSPACE, BindRendererToSpace);
    SetMethod(rendererClass, NEWCTLWINDOW, ShowRendererControls);
    SetVar(rendererClass, DOUBLECLICK, NewString(OF_SHOW_CONTROLS));
    SetVar(rendererClass, CLASSID, NewInt(CLASS_RENDERER));
    SetVar(rendererClass, ONEONLY, ObjTrue);
    SetVar(rendererClass, RENDERTYPE, NewInt(hasRGB ? RT_RGB_HARDWARE : RT_CMAP_HARDWARE));
    SetVar(rendererClass, FILTERTYPE, NewInt(RT_NONE));
    icon = NewIcon(0, 0, ICONRENDERER, "Renderer");
    SetVar(icon, HELPSTRING,
	NewString("This icon represents a renderer.  The controls the process \
of rendering, or producing an image from the visualization objects in the space.  \
You can show controls for the renderer by selecting the icon and choosing the Show Controls \
item in the Object menu.\n\
\n\
You can drag this icon into the icon corral of another visualization window to have \
it control the other space as well.  \
A space can only be controlled by one renderer at a time.  If you drag another \
renderer icon into this space, this renderer will be replaced."));
    SetVar(rendererClass, DEFAULTICON, icon);
    SetVar(rendererClass, NAME, NewString("Renderer"));
    SetMethod(rendererClass, CLONE, CloneRenderer);

    /*Make a placement track class*/
    placementTrackClass = NewObject(subTrackClass, 0L);
    AddToReferenceList(placementTrackClass);
    SetMethod(placementTrackClass, EXPRESS, ExpressObserverPlacement);
    DeclareDependency(placementTrackClass, JOINTS, CHANGED);
    SetMethod(placementTrackClass, JOINTS, MakePlacementJoints);

    /*Make joint class*/
    jointClass = NewObject(NULLOBJ, 0L);
    AddToReferenceList(jointClass);

    InitLights();
}

void KillSpaces()
/*Kills the spaces*/
{
    KillLights();
    RemoveFromReferenceList(jointClass);
    RemoveFromReferenceList(placementTrackClass);
    RemoveFromReferenceList(rendererClass);
    RemoveFromReferenceList(observerClass);
    RemoveFromReferenceList(stereoClass);
    RemoveFromReferenceList(placementClass);
    RemoveFromReferenceList(worldClass);
    RemoveFromReferenceList(clockClass);
    RemoveFromReferenceList(controllerClass);
    RemoveFromReferenceList(spaceBackPanelClass);
    RemoveFromReferenceList(spacePanelClass);
    RemoveFromReferenceList(spaceClass);
}
Modified: Sun Nov 17 17:00:00 1996 GMT
Page accessed 2605 times since Sat Apr 17 21:55:07 1999 GMT