/* <copyright>
 This file contains proprietary software owned by Motorola Mobility, Inc.<br/>
 No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/>
 (c) Copyright 2011 Motorola Mobility, Inc.  All Rights Reserved.
 </copyright> */
 
 ///////////////////////////////////////////////////////////////////////
// Class GLRectangle
//      GL representation of a rectangle.
//      Derived from class GLGeomObj
///////////////////////////////////////////////////////////////////////
function GLRectangle()
{
	// CONSTANTS
	this.N_TRIANGLES = 15;

	// initialize the inherited members
	this.inheritedFrom = GLGeomObj;
	this.inheritedFrom();

	///////////////////////////////////////////////////////////////////////
	// Instance variables
	///////////////////////////////////////////////////////////////////////
	this._width = 2.0;
	this._height = 2.0;
	this._xOffset = 0;
	this._yOffset = 0;

	this._tlRadius = 0;
	this._trRadius = 0;
	this._blRadius = 0;
	this._brRadius = 0;

	this._strokeWidth = 0.25;

	this._strokeStyle = "Solid";
	this.init = function(world, xOffset, yOffset, width, height, strokeSize, strokeColor, fillColor,
                      tlRadius, trRadius, blRadius, brRadius, strokeMaterial, fillMaterial, strokeStyle)
	{


		this.m_world = world;

		if (arguments.length > 0)
		{
			this._width = width;
			this._height = height;
			this._xOffset = xOffset;
			this._yOffset = yOffset;

			this._strokeWidth = strokeSize;
			this._strokeColor = strokeColor;
			this._fillColor = fillColor;

			this.setTLRadius(tlRadius);
			this.setTRRadius(trRadius);
			this.setBLRadius(blRadius);
			this.setBRRadius(brRadius);

			this._strokeStyle = strokeStyle;
		}

		// the overall radius includes the fill and the stroke.  separate the two based onthe stroke width
		//  this._fillRad = this._radius - this._strokeWidth;
		//    var err = 0.05;
		var err = 0;
		this._fillWidth = this._width - this._strokeWidth  + err;
		this._fillHeight = this._height - this._strokeWidth + err;

		this._materialAmbient  = [0.2, 0.2, 0.2,  1.0];
		this._materialDiffuse  = [0.4, 0.4, 0.4,  1.0];
		this._materialSpecular = [0.4, 0.4, 0.4,  1.0];

		if(strokeMaterial)
			this._strokeMaterial = strokeMaterial;
		else
			this._strokeMaterial = new FlatMaterial();

		if(fillMaterial)
			this._fillMaterial = fillMaterial;
		else 
			this._fillMaterial = new FlatMaterial();
	}

	///////////////////////////////////////////////////////////////////////
	// Property Accessors
	///////////////////////////////////////////////////////////////////////
	this.getStrokeWidth   = function()    {  return this._strokeWidth;    }
	this.setStrokeWidth   = function(w)   {  this._strokeWidth = w;     }

	this.getStrokeMaterial  = function()    {  return this._strokeMaterial;   }
	this.setStrokeMaterial  = function(m)   {  this._strokeMaterial = m;    }

	this.getFillMaterial  = function()    {  return this._fillMaterial;   }
	this.setFillMaterial  = function(m)   {  this._fillMaterial = m;      }

	this.getStrokeColor   = function()    {  return this._strokeColor;    }
	//this.setStrokeColor   = function(c)   {  this._strokeColor = c;     }

	this.getFillColor   = function()    {  return this._fillColor;      }
	//this.setFillColor   = function(c)	{  this._fillColor = c.slice(0);	}

	this.getTLRadius    = function()    {  return this._tlRadius;     }
	this.setTLRadius    = function(r)   {  this._tlRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);   }

	this.getTRRadius    = function()    {  return this._trRadius;     }
	this.setTRRadius    = function(r)   {  this._trRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);   }

	this.getBLRadius    = function()    {  return this._blRadius;     }
	this.setBLRadius    = function(r)   {  this._blRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);   }

	this.getBRRadius    = function()    {  return this._brRadius;     }
	this.setBRRadius    = function(r)   {  this._brRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);   }

	this.getStrokeStyle   = function()    {  return this._strokeStyle;    }
	this.setStrokeStyle   = function(s)   {  this._strokeStyle = s;     }

	this.getWidth     = function()    {  return this._width;        }
	this.setWidth     = function(w)   {  this._width = w;         }

	this.getHeight      = function()    {  return this._height;       }
	this.setHeight      = function(h)   {  this._height = h;        }

	this.geomType     = function()    {  return this.GEOM_TYPE_RECTANGLE; }


	///////////////////////////////////////////////////////////////////////
	// Methods
	///////////////////////////////////////////////////////////////////////
	this.export = function()
	{
		var rtnStr = "type: " + this.geomType() + "\n";

		/////////////////////////////////////////////////////////////////////////
		//
		//	world, xOffset, yOffset, width, height, strokeSize, strokeColor, fillColor,
        //    tlRadius, trRadius, blRadius, brRadius, strokeMaterial, fillMaterial, strokeStyle
		//
		/////////////////////////////////////////////////////////////////////////////

		rtnStr += "xoff: "			+ this._xOffset		+ "\n";
		rtnStr += "yoff: "			+ this._yOffset		+ "\n";
		rtnStr += "width: "			+ this._width		+ "\n";
		rtnStr += "height: "		+ this._height		+ "\n";
		rtnStr += "strokeWidth: "	+ this._strokeWidth	+ "\n";
		rtnStr += "strokeColor: "	+ String(this._strokeColor)  + "\n";
		rtnStr += "fillColor: "		+ String(this._fillColor)	 + "\n";
		rtnStr += "tlRadius: "		+ this._tlRadius	+ "\n";
		rtnStr += "trRadius: "		+ this._trRadius	+ "\n";
		rtnStr += "blRadius: "		+ this._blRadius	+ "\n";
		rtnStr += "brRadius: "		+ this._brRadius	+ "\n";
		rtnStr += "innerRadius: "	+ this._innerRadius	+ "\n";
		rtnStr += "strokeStyle: "	+ this._strokeStyle	+ "\n";

		rtnStr += "strokeMat: ";
		if (this._strokeMaterial)
			rtnStr += this._strokeMaterial.getName();
		else
			rtnStr += "flatMaterial";
		rtnStr += "\n";

		rtnStr += "fillMat: ";
		if (this._fillMaterial)
			rtnStr += this._fillMaterial.getName();
		else
			rtnStr += "flatMaterial";
		rtnStr += "\n";

		return rtnStr;
	}

	this.import = function( importStr )
	{
		this._xOffset			= Number( this.getPropertyFromString( "xoff: ",			importStr )  );
		this._yOffset			= Number( this.getPropertyFromString( "yoff: ",			importStr )  );
		this._width				= Number( this.getPropertyFromString( "width: ",		importStr )  );
		this._height			= Number( this.getPropertyFromString( "height: ",		importStr )  );
		this._strokeWidth		= Number( this.getPropertyFromString( "strokeWidth: ",	importStr )  );
		this._innerRadius		= Number( this.getPropertyFromString( "innerRadius: ",	importStr )  );
		this._strokeStyle		= Number( this.getPropertyFromString( "strokeStyle: ",	importStr )  );
		var strokeMaterialName	= this.getPropertyFromString( "strokeMat: ",	importStr );
		var fillMaterialName	= this.getPropertyFromString( "fillMat: ",		importStr );
		this._strokeStyle		=  this.getPropertyFromString( "strokeStyle: ",	importStr );
		this._fillColor			=  eval( "[" + this.getPropertyFromString( "fillColor: ",	importStr ) + "]" );
		this._strokeColor		=  eval( "[" + this.getPropertyFromString( "strokeColor: ",	importStr ) + "]" );
		this._tlRadius			=  Number( this.getPropertyFromString( "tlRadius: ",	importStr )  );
		this._trRadius			=  Number( this.getPropertyFromString( "trRadius: ",	importStr )  );
		this._blRadius			=  Number( this.getPropertyFromString( "blRadius: ",	importStr )  );
		this._brRadius			=  Number( this.getPropertyFromString( "brRadius: ",	importStr )  );

		var strokeMat = MaterialsLibrary.getMaterial( strokeMaterialName );
		if (!strokeMat)
		{
			console.log( "object material not found in library: " + strokeMaterialName );
			strokeMat = new FlatMaterial();
		}
		this._strokeMaterial = strokeMat;

		var fillMat = MaterialsLibrary.getMaterial( fillMaterialName );
		if (!fillMat)
		{
			console.log( "object material not found in library: " + fillMaterialName );
			fillMat = new FlatMaterial();
		}
		this._fillMaterial = fillMat;
	}

	this.buildBuffers = function()
	{
		// get the world
		var world = this.getWorld();
		if (!world)  throw( "null world in buildBuffers" );
		//console.log( "GLRectangle.buildBuffers " + world._worldCount );
		if (!world._useWebGL)  return;
		
		// make sure RDGE has the correct context
		g_Engine.setContext( world.getCanvas().rdgeid );

		// create the gl buffer
		var gl = world.getGLContext();

		var tlRadius = this._tlRadius; //top-left radius
		var trRadius = this._trRadius;
		var blRadius = this._blRadius;
		var brRadius = this._brRadius;

		// declare the arrays to hold the parts
        this._primArray = [];
    	this._materialArray = [];
		this._materialTypeArray = [];
	    this._materialNodeArray = [];

		// get the normalized device coordinates (NDC) for
		// all position and dimensions.
		var vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
		var xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
			xFillNDC = this._width/vpw,  yFillNDC = this._height/vph,
			strokeSizeNDC = 2*this._strokeWidth/vpw,
			tlRadiusNDC = 2*tlRadius/vpw,  yTLRadiusNDC = 2*tlRadius/vph,
			trRadiusNDC = 2*trRadius/vpw,  yTRRadiusNDC = 2*trRadius/vph,
			blRadiusNDC = 2*blRadius/vpw,  yBLRadiusNDC = 2*blRadius/vph,
			brRadiusNDC = 2*brRadius/vpw,  yBRRadiusNDC = 2*brRadius/vph;

		var aspect = world.getAspect();
		var zn = world.getZNear(),  zf = world.getZFar();
		var t = zn * Math.tan(world.getFOV() * Math.PI / 360.0),
			b = -t,
			r = aspect*t,
			l = -r;

		// calculate the object coordinates from their NDC coordinates
		var z = -world.getViewDistance();

		// get the position of the origin
		var x = -z*(r-l)/(2.0*zn)*xNDC,
			y = -z*(t-b)/(2.0*zn)*yNDC;

		// get the x and y fill
		var xFill = -z*(r-l)/(2.0*zn)*xFillNDC,
			yFill = -z*(t-b)/(2.0*zn)*yFillNDC;

		// keep some variables giving the overall dimensions of the
		// rectangle. These values are used to calculate consistent
		// texture map coordinates across all pieces.
		this._rectWidth = xFill;  this._rectHeight = yFill;

		// get the stroke size
		var strokeSize = -z*(r-l)/(2.0*zn)*strokeSizeNDC;

		// get the absolute corner radii
		tlRadius = -z*(r-l)/(2.0*zn)*tlRadiusNDC,
		trRadius = -z*(r-l)/(2.0*zn)*trRadiusNDC,
		blRadius = -z*(r-l)/(2.0*zn)*blRadiusNDC,
		brRadius = -z*(r-l)/(2.0*zn)*brRadiusNDC;

		// stroke
		var strokeMaterial = this.makeStrokeMaterial();
		var strokePrim = this.createStroke([x,y],  2*xFill,  2*yFill,  strokeSize,  tlRadius, blRadius, brRadius, trRadius, strokeMaterial);
        this._primArray.push( strokePrim );
		this._materialNodeArray.push( strokeMaterial.getMaterialNode() );

		// fill
		tlRadius -= strokeSize;  if (tlRadius < 0)  tlRadius = 0.0;
		blRadius -= strokeSize;  if (blRadius < 0)  blRadius = 0.0;
		brRadius -= strokeSize;  if (brRadius < 0)  brRadius = 0.0;
		trRadius -= strokeSize;  if (trRadius < 0)  trRadius = 0.0;
		xFill -= strokeSize;
		yFill -= strokeSize;
		var fillMaterial = this.makeFillMaterial();
		//console.log( "fillMaterial: " + fillMaterial.getName() );
		var fillPrim = this.createFill([x,y],  2*xFill,  2*yFill,  tlRadius, blRadius, brRadius, trRadius, fillMaterial);
        this._primArray.push( fillPrim );
		this._materialNodeArray.push( fillMaterial.getMaterialNode() );

        world.updateObject(this);
	}

	this.renderQuadraticBezier = function( bPts, ctx )
	{
		if (!bPts)  return;

		var nSegs = (bPts.length - 1)/2.0;
		if (nSegs <= 0)  return;

		var index = 1;
		for (var i=0;  i<nSegs;  i++)
		{
			ctx.quadraticCurveTo(  bPts[index][0],  bPts[index][1],    bPts[index+1][0], bPts[index+1][1] );
			index += 2;
		}
	}

	this.renderPath = function( inset, ctx )
	{
		// various declarations
		var pt,  rad,  ctr,  startPt, bPts;
		var width  = Math.round(this.getWidth()),
			height = Math.round(this.getHeight());

		pt = [inset, inset];	// top left corner

		var tlRad = this._tlRadius; //top-left radius
		var trRad = this._trRadius;
		var blRad = this._blRadius;
		var brRad = this._brRadius;

		if ((tlRad <= 0) && (blRad <= 0) && (brRad <= 0) && (trRad <= 0))
		{
			ctx.rect(pt[0], pt[1], width - 2*inset, height - 2*inset);
		}
		else
		{
			// get the top left point
			rad = tlRad - inset;
			if (rad < 0)  rad = 0;
			pt[1] += rad;
			if (MathUtils.fpSign(rad) == 0)  pt[1] = inset;
			ctx.moveTo( pt[0],  pt[1] );

			// get the bottom left point
			pt = [inset, height - inset];
			rad = blRad - inset;
			if (rad < 0)  rad = 0;
			pt[1] -= rad;
			ctx.lineTo( pt[0],  pt[1] );

			// get the bottom left curve
			if (MathUtils.fpSign(rad) > 0)
				ctx.quadraticCurveTo( inset, height-inset,  inset+rad, height-inset );

			// do the bottom of the rectangle
			pt = [width - inset,  height - inset];
			rad = brRad - inset;
			if (rad < 0)  rad = 0;
			pt[0] -= rad;
			ctx.lineTo( pt[0], pt[1] );

			// get the bottom right arc
			if (MathUtils.fpSign(rad) > 0)
				ctx.quadraticCurveTo( width-inset, height-inset,  width-inset, height-inset-rad );

			// get the right of the rectangle
			pt = [width - inset,  inset];
			rad = trRad - inset;
			if (rad < 0)  rad = 0;
			pt[1] += rad;
			ctx.lineTo( pt[0], pt[1] );

			// do the top right corner
			if (MathUtils.fpSign(rad) > 0)
				ctx.quadraticCurveTo( width-inset, inset,  width-inset-rad, inset );

			// do the top of the rectangle
			pt = [inset, inset]
			rad = tlRad - inset;
			if (rad < 0)  rad = 0;
			pt[0] += rad;
			ctx.lineTo( pt[0], pt[1] );

			// do the top left corner
			if (MathUtils.fpSign(rad) > 0)
				ctx.quadraticCurveTo( inset, inset, inset, inset+rad );
			else
				ctx.lineTo( inset, 2*inset );
		}
	}

    this.render = function()
    {
        // get the world
        var world = this.getWorld();
        if (!world)  throw( "null world in rectangle render" );

         // get the context
		var ctx = world.get2DContext();
		if (!ctx)  return;

		// get some dimensions
		var lw = this._strokeWidth;
		var	w = world.getViewportWidth(),
			h = world.getViewportHeight();
		
		// render the fill
		ctx.beginPath();
		if (this._fillColor)
		{
			var c = "rgba(" + 255*this._fillColor[0] + "," + 255*this._fillColor[1] + "," + 255*this._fillColor[2] + "," + this._fillColor[3] + ")";  
			ctx.fillStyle = c;

			ctx.lineWidth	= lw;
			var inset = Math.ceil( lw ) + 0.5;
			this.renderPath( inset, ctx );
			ctx.fill();
			ctx.closePath();
		}

		// render the stroke
		ctx.beginPath();
		if (this._strokeColor)
		{
			var c = "rgba(" + 255*this._strokeColor[0] + "," + 255*this._strokeColor[1] + "," + 255*this._strokeColor[2] + "," + this._strokeColor[3] + ")";  
			ctx.strokeStyle = c;

			ctx.lineWidth	= lw;
			var inset = Math.ceil( 0.5*lw ) + 0.5;
			this.renderPath( inset, ctx );
			ctx.stroke();
			ctx.closePath();
		}
    }

	this.createStroke = function(ctr,  width,  height,  strokeWidth,  tlRad, blRad, brRad, trRad, material)
	{
		// create the geometry
		var prim = RectangleStroke.create( ctr,  width, height, strokeWidth,  tlRad, blRad,  brRad, trRad, material)
		return prim;
	}

	this.createFill = function( ctr,  width,  height,  tlRad, blRad, brRad, trRad, material)
	{
		// create the geometry
		// special the (common) case of no rounded corners
		var prim
		if ((tlRad <= 0) && (blRad <= 0) && (brRad <= 0) && (trRad <= 0))
			prim = RectangleGeometry.create( ctr, width, height, material );
		else
			prim = RectangleFill.create( ctr,  width, height,  tlRad, blRad,  brRad, trRad, material);

		return prim;
	}

	this.collidesWithPoint = function( x, y )
	{
		if(x < this._xOffset) return false;
		if(x > (this._xOffset + this._width)) return false;
		if(y < this._yOffset) return false;
		if(y > (this._yOffset + this._height)) return false;

		return true;
	}
	
    this.containsPoint = function( pt,  dir )
    {
        var world = this.getWorld();
        if (!world)  throw( "null world in containsPoint" );
		
		// get a point on the plane of the circle
		// the point is in NDC, as is the input parameters
		var mat = this.getMatrix();
		var plane = Vector.create([0,0,1,0]);
		plane = MathUtils.transformPlane( plane, mat );
		var projPt = MathUtils.vecIntersectPlane ( pt, dir, plane );

		// transform the projected point back to the XY plane
		//var invMat = mat.inverse();
		var invMat = glmat4.inverse( mat, [] );
		var planePt = MathUtils.transformPoint( projPt, invMat );

		// get the normalized device coordinates (NDC) for
		// the position and radii.
		var	vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
		var	xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
			xRadNDC = this._width/vpw,  yRadNDC = this._height/vph;
		var projMat = world.makePerspectiveMatrix();
		var z = -world.getViewDistance();
		var planePtNDC = planePt.slice(0);
		planePtNDC[2] = z;
		planePtNDC = MathUtils.transformHomogeneousPoint( planePtNDC, projMat );
		planePtNDC = MathUtils.applyHomogeneousCoordinate( planePtNDC );

		// get the center and dimensions of the rect in NDC
		var vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
		var xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
			hw = this._width/vpw,  hh = this._height/vph;

		var x = planePtNDC[0],  y = planePtNDC[1];
		if (x < (xNDC - hw))  return false;
		if (x > (xNDC + hw))  return false;
		if (y < (yNDC - hh))  return false;
		if (y > (yNDC + hh))  return false;

		return true;
	}

	this.getNearVertex = function( pt, dir )
	{
        var world = this.getWorld();
        if (!world)  throw( "null world in getNearPoint" );
		
		// get a point on the plane of the circle
		// the point is in NDC, as is the input parameters
		var mat = this.getMatrix();
		var plane = Vector.create([0,0,1,0]);
		plane = MathUtils.transformPlane( plane, mat );
		var projPt = MathUtils.vecIntersectPlane ( pt, dir, plane );

		// transform the projected point back to the XY plane
		//var invMat = mat.inverse();
		var invMat = glmat4.inverse(mat, []);
		var planePt = MathUtils.transformPoint( projPt, invMat );

		// get the normalized device coordinates (NDC) for
		// the position and radii.
		var	vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
		var	xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
			hwNDC = this._width/vpw,  hhNDC = this._height/vph;
		var projMat = world.makePerspectiveMatrix();
		var z = -world.getViewDistance();
		var planePtNDC = planePt.slice(0);
		planePtNDC[2] = z;
		planePtNDC = MathUtils.transformHomogeneousPoint( planePtNDC, projMat );
		planePtNDC = MathUtils.applyHomogeneousCoordinate( planePtNDC );

		// get the near point in NDC
		var x = planePtNDC[0],  y = planePtNDC[1];
		var xMin = xNDC - hwNDC,  xMax = xNDC + hwNDC,
			yMin = yNDC - hhNDC,  yMax = yNDC + hhNDC;

		// compare the point against the 4 corners
			var pt, dist;
			pt = Vector.create( [xMin, yMin, 0] );
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			var minPt = pt,  minDist = dist;
		
			pt = Vector.create( [xMin, yMax, 0] );
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			if (dist < minDist)
			{
				minDist = dist;
				minPt = pt;
			}

			pt = Vector.create( [xMax, yMax, 0] );
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			if (dist < minDist)
			{
				minDist = dist;
				minPt = pt;
			}
		
			pt = Vector.create( [xMax, yMin, 0] );
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			if (dist < minDist)
			{
				minDist = dist;
				minPt = pt;
			}
		
		// convert to GL coordinates
		x = minPt[0];  y = minPt[1];
		var aspect = world.getAspect();
		var zn = world.getZNear(),  zf = world.getZFar();
		var	t = zn * Math.tan(world.getFOV() * Math.PI / 360.0),
			b = -t,
			r = aspect*t,
			l = -r;
		var objPt = Vector.create([0,0,0]);
		objPt[0] = -z*(r-l)/(2.0*zn)*x;
		objPt[1] = -z*(t-b)/(2.0*zn)*y;

		// re-apply the transform
		objPt = MathUtils.transformPoint( objPt, mat );

		return objPt;
	}
	
    this.getNearPoint = function( pt, dir )
    {
        var world = this.getWorld();
        if (!world)  throw( "null world in getNearPoint" );
		
		// get a point on the plane of the circle
		// the point is in NDC, as is the input parameters
		var mat = this.getMatrix();
		var plane = Vector.create([0,0,1,0]);
		plane = MathUtils.transformPlane( plane, mat );
		var projPt = MathUtils.vecIntersectPlane ( pt, dir, plane );

		// transform the projected point back to the XY plane
		//var invMat = mat.inverse();
		var invMat = glmat4.inverse(mat, []);
		var planePt = MathUtils.transformPoint( projPt, invMat );

		// get the normalized device coordinates (NDC) for
		// the position and radii.
		var	vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
		var	xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
			hwNDC = this._width/vpw,  hhNDC = this._height/vph;
		var projMat = world.makePerspectiveMatrix();
		var z = -world.getViewDistance();
		var planePtNDC = planePt.slice(0);
		planePtNDC[2] = z;
		planePtNDC = MathUtils.transformHomogeneousPoint( planePtNDC, projMat );
		planePtNDC = MathUtils.applyHomogeneousCoordinate( planePtNDC );

		// get the near point in NDC
		var x = planePtNDC[0],  y = planePtNDC[1];
		var xMin = xNDC - hwNDC,  xMax = xNDC + hwNDC,
			yMin = yNDC - hhNDC,  yMax = yNDC + hhNDC;

		// compare the point against the near point on the 4 sides
			var pt, dist;
			pt = Vector.create( [xMin, y, 0] );
			if      (pt[1] < yMin)  pt[1] = yMin;
			else if (pt[1] > yMax)  pt[1] = yMax;
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			var minPt = pt,  minDist = dist;
		
			pt = Vector.create( [x, yMax, 0] );
			if      (pt[0] < xMin)  pt[0] = xMin;
			else if (pt[0] > xMax)  pt[0] = xMax;
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			if (dist < minDist)
			{
				minDist = dist;
				minPt = pt;
			}

			pt = Vector.create( [xMax, y, 0] );
			if      (pt[1] < yMin)  pt[1] = yMin;
			else if (pt[1] > yMax)  pt[1] = yMax;
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			if (dist < minDist)
			{
				minDist = dist;
				minPt = pt;
			}
		
			pt = Vector.create( [x, yMin, 0] );
			if      (pt[0] < xMin)  pt[0] = xMin;
			else if (pt[0] > xMax)  pt[0] = xMax;
			dist = VecUtils.vecDist(2, pt, planePtNDC);
			if (dist < minDist)
			{
				minDist = dist;
				minPt = pt;
			}
		
		// convert to GL coordinates
		x = minPt[0];  y = minPt[1];
		var aspect = world.getAspect();
		var zn = world.getZNear(),  zf = world.getZFar();
		var	t = zn * Math.tan(world.getFOV() * Math.PI / 360.0),
			b = -t,
			r = aspect*t,
			l = -r;
		var objPt = Vector.create([0,0,0]);
		objPt[0] = -z*(r-l)/(2.0*zn)*x;
		objPt[1] = -z*(t-b)/(2.0*zn)*y;

		// re-apply the transform
		objPt = MathUtils.transformPoint( objPt, mat );

		return objPt;
    }


	 this.recalcTexMapCoords = function( vrts, uvs )
	 {
		var n = vrts.length/3;
		var ivrt = 0,  iuv = 0;
		for (var i=0;  i<n;  i++)
		{
			uvs[iuv] = 0.5*(vrts[ivrt]/this._rectWidth + 1);
			iuv++;  ivrt++;
			uvs[iuv] = 0.5*(vrts[ivrt]/this._rectHeight + 1);
			iuv++;  ivrt += 2;
		}

		//console.log( "remap: " + uvs );
	 }

 }

RectangleFill = {};
RectangleFill.create = function( rectCtr,  width, height, tlRad, blRad,  brRad, trRad,  material)
{
	var x = rectCtr[0],  y = rectCtr[1],  z = 0.0;
	var	hw = 0.5*width,  hh = 0.5*height;

	// limit the radii to half the rectangle dimension
	var minDimen = hw < hh ? hw : hh;
	if (tlRad > minDimen)  tlRad = minDimen;
	if (blRad > minDimen)  blRad = minDimen;
	if (brRad > minDimen)  brRad = minDimen;
	if (trRad > minDimen)  trRad = minDimen;

	// define some local variables
	this.vertices	= [];
	this.normals	= [];
	this.uvs		= [];
	this.indices	= [];
	
	// the center of the rectangle is the first vertex
	RectangleFill.pushVertex( x, y, z );

	// traverse the perimiter of the rectangle

	// push the starting point
	RectangleFill.pushVertex( x-hw, y+hh-tlRad,  z);

	// do the left side
	var ctr;
	if (blRad <= 0)
		RectangleFill.pushVertex( x-hw, y-hh, z);
	else
	{
		ctr = [x - hw + blRad,  y - hh + blRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x-hw, y-hh+blRad, z],  this.vertices );
	}

	// do the bottom
	if (brRad <= 0)
		RectangleFill.pushVertex( x+hw, y-hh, z);
	else
	{
		ctr = [x + hw - brRad,  y - hh + brRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x+hw-brRad, y-hh, z],  this.vertices );
	}

	// do the right
	if (trRad <= 0)
		RectangleFill.pushVertex( x+hw, y+hh, z);
	else
	{
		ctr = [x + hw - trRad,  y + hh - trRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x+hw, y+hh-trRad, z],  this.vertices );
	}

	// do the top
	if (tlRad <= 0)
		RectangleFill.pushVertex( x-hw, y+hh, z);
	else
	{
		ctr = [x - hw + tlRad,  y + hh - tlRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x-hw+tlRad, y+hh, z],  this.vertices );
	}

	// get the normals and uvs
	var vrt, uv;
	var xMin = x - hw,
		yMin = y - hh;
	var n = [0, 0, 1];
	var nVertices = this.vertices.length / 3;
	for (var i=0;  i<nVertices;  i++)
	{
		vrt = RectangleFill.getVertex(i);
		RectangleFill.pushNormal( n );
		uv  = RectangleFill.getUV(vrt[0], vrt[1], xMin, width, yMin, height);
		RectangleFill.pushUV( uv );
	}

	// build the triangles
	var nTriangles = nVertices - 2;
	var i = 1,  j = 2;
	for (var iTri=0;  iTri<nTriangles;  iTri++)
	{
		RectangleFill.pushIndices( 0, j, i );
		i++;
		j++;
	}

	//refine the mesh for vertex deformations
//	if (material)
//	{
//		if (material.hasVertexDeformation())
//		{
//			var paramRange = material.getVertexDeformationRange();
//			var tolerance = material.getVertexDeformationTolerance();
//			nVertices = ShapePrimitive.refineMesh( this.vertices, this.normals, this.uvs, this.indices, nVertices,  paramRange,  tolerance );
//		}
//	}

	// create the RDGE primitive
	var prim = ShapePrimitive.create(this.vertices, this.normals, this.uvs, this.indices, g_Engine.getContext().renderer.TRIANGLES, nVertices);
	return prim;
}

RectangleFill.pushVertex = function( x, y, z )
{
	this.vertices.push( x );
	this.vertices.push( y );
	this.vertices.push( z );
}

RectangleFill.pushNormal = function( n )
{
	this.normals.push( n[0] );
	this.normals.push( n[1] );
	this.normals.push( n[2] );
}

RectangleFill.pushUV = function( uv )
{
	this.uvs.push( uv[0] );
	this.uvs.push( uv[1] );
}

RectangleFill.pushIndices = function( i, j, k )
{
	this.indices.push( i );
	this.indices.push( j );
	this.indices.push( k );
}

RectangleFill.getVertex = function( index )
{
	var i = 3*index;
	return [ this.vertices[i],  this.vertices[i+1],  this.vertices[i+2] ];
}

RectangleFill.getUV = function( x, y, xMin, w, yMin, h)
{
	var u = (x - xMin)/w,
		v = (y - yMin)/h;

	var uv = [ u, v ];
	return uv;
}

RectangleFill.getRoundedCorner = function(ctr, startPt,  vertices)
{
	var pt0 = startPt.slice();

	// create a matrix to rotate about the center
	var nSegs = 16;
	var angle = 0.5*Math.PI/nSegs;
	var ctrNeg = ctr.slice();
	VecUtils.vecNegate(3, ctrNeg);
	var tNeg  = Matrix.Translation( ctrNeg ),
		rot   = Matrix.RotationZ( angle ),
		trans = Matrix.Translation( ctr );
	var mat = glmat4.multiply( rot, tNeg, [] );
	glmat4.multiply(trans, mat, mat );

	RectangleFill.pushVertex(pt0[0], pt0[1], 0.0 );
	for (var i=0;  i<nSegs;  i++)
	{
		pt0 = MathUtils.transformPoint( pt0, mat );
		RectangleFill.pushVertex(pt0[0], pt0[1], 0.0 );
	}
}


RectangleStroke = {};
RectangleStroke.create = function( rectCtr,  width, height, strokeWidth,  tlRad, blRad,  brRad, trRad, material)
{
	var x = rectCtr[0],  y = rectCtr[1],  z = 0.0;
	var	hw = 0.5*width,  hh = 0.5*height,  sw = strokeWidth;

	// limit the radii to half the rectangle dimension
	var minDimen = hw < hh ? hw : hh;
	if (tlRad > minDimen)  tlRad = minDimen;
	if (blRad > minDimen)  blRad = minDimen;
	if (brRad > minDimen)  brRad = minDimen;
	if (trRad > minDimen)  trRad = minDimen;

	// define some local variables
	this.vertices	= [];
	this.normals	= [];
	this.uvs		= [];
	this.indices	= [];

	// get the starting points
	if (tlRad == 0)
	{
		RectangleStroke.pushVertex( x-hw+sw, y+hh-sw, z);
		RectangleStroke.pushVertex( x-hw,    y+hh,    z);
	}
	else
	{
		if (tlRad > sw)
		{
			RectangleStroke.pushVertex( x-hw+sw, y+hh-tlRad, z);
			RectangleStroke.pushVertex( x-hw,    y+hh-tlRad, z);
		}
		else
		{
			RectangleStroke.pushVertex( x-hw+tlRad, y+hh-tlRad, z);
			RectangleStroke.pushVertex( x-hw,       y+hh-tlRad, z);
			RectangleStroke.pushVertex( x-hw+sw,    y+hh-sw,    z);
			RectangleStroke.pushVertex( x-hw,       y+hh-sw,    z);
		}
	}

	// get the left side
	if (blRad == 0)
	{
		RectangleStroke.pushVertex( x-hw+sw, y-hh+sw, z);
		RectangleStroke.pushVertex( x-hw,    y-hh,    z);
	}
	else
	{
		if (blRad >= sw)
		{
			RectangleStroke.pushVertex( x-hw+sw, y-hh+blRad, z);
			RectangleStroke.pushVertex( x-hw,    y-hh+blRad, z);
			var ctr		 =	[x-hw+blRad,  y-hh+blRad, z],
				insidePt =	[x-hw+sw,     y-hh+blRad, z],
				outsidePt = [x-hw,        y-hh+blRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		}
		else
		{
			RectangleStroke.pushVertex( x-hw+sw,  y-hh+sw,    z);
			RectangleStroke.pushVertex( x-hw,     y-hh+blRad, z);
			var ctr		 =	[x-hw+blRad,  y-hh+blRad, z],
				insidePt =	[x-hw+blRad,  y-hh+blRad, z],
				outsidePt = [x-hw,        y-hh+blRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt, this.vertices  );

			RectangleStroke.pushVertex( x-hw+sw,  y-hh+sw, z);
			RectangleStroke.pushVertex( x-hw+sw,  y-hh,    z);
		}
	}

	// get the bottom
	if (brRad == 0)
	{
		RectangleStroke.pushVertex( x+hw-sw, y-hh+sw, z);
		RectangleStroke.pushVertex( x+hw,    y-hh,    z);
	}
	else
	{
		RectangleStroke.pushVertex( x+hw-brRad,    y-hh+sw, z);
		RectangleStroke.pushVertex( x+hw-brRad,    y-hh,    z);
		if (brRad >= sw)
		{
			var ctr		 =	[x+hw-brRad,  y-hh+brRad, z],
				insidePt =	[x+hw-brRad,  y-hh+sw,    z],
				outsidePt = [x+hw-brRad,  y-hh,       z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		}
		else
		{
			RectangleStroke.pushVertex( x+hw-sw,    y-hh+sw, z);
			RectangleStroke.pushVertex( x+hw-brRad,    y-hh, z);
			var ctr		 =	[x+hw-brRad,  y-hh+brRad, z],
				insidePt =	[x+hw-brRad,  y-hh+brRad, z],
				outsidePt = [x+hw-brRad,  y-hh,       z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt,  this.vertices );
			RectangleStroke.pushVertex( x+hw-sw,    y-hh+sw, z);
			RectangleStroke.pushVertex( x+hw,       y-hh+sw, z);
		}
	}

	// get the right
	if (trRad == 0)
	{
		RectangleStroke.pushVertex( x+hw-sw, y+hh-sw, z);
		RectangleStroke.pushVertex(    x+hw,    y+hh, z);
	}
	else
	{
		if (trRad >= sw)
		{
			RectangleStroke.pushVertex( x+hw-sw,  y+hh-trRad, z);
			RectangleStroke.pushVertex( x+hw,     y+hh-trRad, z);
			var ctr		 =	[x+hw-trRad,  y+hh-trRad, z],
				insidePt =	[x+hw-sw,     y+hh-trRad, z],
				outsidePt = [x+hw,        y+hh-trRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		}
		else
		{
			RectangleStroke.pushVertex( x+hw-sw,  y+hh-sw,    z);
			RectangleStroke.pushVertex( x+hw,     y+hh-trRad, z);
			var ctr		 =	[x+hw-trRad,  y+hh-trRad, z],
				insidePt =	[x+hw-trRad,  y+hh-trRad, z],
				outsidePt = [x+hw,        y+hh-trRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt,  this.vertices );
			RectangleStroke.pushVertex( x+hw-sw,  y+hh-sw, z);
			RectangleStroke.pushVertex( x+hw-sw,  y+hh,    z);
		}
	}

	// get the top
	if (tlRad == 0)
	{
		RectangleStroke.pushVertex( x-hw+sw,  y+hh-sw, z);
		RectangleStroke.pushVertex( x-hw,     y+hh,    z);
	}
	else
	{
		if (tlRad >= sw)
		{
			RectangleStroke.pushVertex( x-hw+tlRad,  y+hh-sw, z);
			RectangleStroke.pushVertex( x-hw+tlRad,  y+hh,    z);
			var ctr		 =	[x-hw+tlRad,  y+hh-tlRad, z],
				insidePt =	[x-hw+tlRad,  y+hh-sw, z],
				outsidePt = [x-hw+tlRad,  y+hh, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		}
		else
		{
			RectangleStroke.pushVertex( x-hw+sw,     y+hh-sw, z);
			RectangleStroke.pushVertex( x-hw+tlRad,  y+hh,    z);
			var ctr		 =	[x-hw+tlRad,  y+hh-tlRad, z],
				insidePt =	[x-hw+tlRad,  y+hh-tlRad, z],
				outsidePt = [x-hw+tlRad,  y+hh, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt,  this.vertices );
		}
	}

	// get the normals and uvs
	var vrt, uv;
	var xMin = x - hw,
		yMin = y - hh;
	var n = [0, 0, 1];
	var nVertices = this.vertices.length / 3;
	for (var i=0;  i<nVertices;  i++)
	{
		vrt = RectangleStroke.getVertex(i);
		RectangleStroke.pushNormal( n );
		uv  = RectangleStroke.getUV(vrt[0], vrt[1], xMin, width, yMin, height);
		RectangleStroke.pushUV( uv );
	}

	// build the triangles
	var nTriangles = nVertices - 2;
	var i = 0,  j = 1, k = 2;
	var reverse = false;
	for (var iTri=0;  iTri<nTriangles;  iTri++)
	{
		// we created a triangle strip, so each sequential triangle has the opposite orientation than its predecessor
		if (!reverse)
			RectangleStroke.pushIndices( k, j, i );
		else
			RectangleStroke.pushIndices( i, j, k );
		reverse = !reverse;

		i++;
		j++;
		k++;
	}

	//refine the mesh for vertex deformations
//	if (material)
//	{
//		if (material.hasVertexDeformation())
//		{
//			var paramRange = material.getVertexDeformationRange();
//			var tolerance = material.getVertexDeformationTolerance();
//			nVertices = ShapePrimitive.refineMesh( this.vertices, this.normals, this.uvs, this.indices, nVertices,  paramRange,  tolerance );
//		}
//	}

	// create the RDGE primitive
	var prim = ShapePrimitive.create(this.vertices, this.normals, this.uvs, this.indices, g_Engine.getContext().renderer.TRIANGLES, nVertices);
	return prim;
}

RectangleStroke.getRoundedCorner = function( ctr, insidePt, outsidePt )
{
	var pt0 = insidePt.slice(),
		pt1 = outsidePt.slice();

	// create a matrix to rotate about the center
	var nSegs = 16;
	var angle = 0.5*Math.PI/nSegs;
	var ctrNeg = ctr.slice();
	VecUtils.vecNegate(3, ctrNeg);
	var tNeg  = Matrix.Translation( ctrNeg ),
		rot   = Matrix.RotationZ( angle ),
		trans = Matrix.Translation( ctr );
	var mat = glmat4.multiply( rot, tNeg, [] );
	glmat4.multiply(trans, mat, mat );

	RectangleStroke.pushVertex(pt0[0], pt0[1], 0.0 );
	RectangleStroke.pushVertex(pt1[0], pt1[1], 0.0 );
	for (var i=0;  i<nSegs;  i++)
	{
		pt0 = MathUtils.transformPoint( pt0, mat );
		pt1 = MathUtils.transformPoint( pt1, mat );

		RectangleStroke.pushVertex(pt0[0], pt0[1], 0.0 );
		RectangleStroke.pushVertex(pt1[0], pt1[1], 0.0 );
	}
}

RectangleStroke.pushVertex	= RectangleFill.pushVertex;
RectangleStroke.pushNormal	= RectangleFill.pushNormal;
RectangleStroke.pushUV		= RectangleFill.pushUV;
RectangleStroke.pushIndices	= RectangleFill.pushIndices;
RectangleStroke.getVertex	= RectangleFill.getVertex;
RectangleStroke.getUV		= RectangleFill.getUV;



// Helper function for generating Three.js geometry
RectangleGeometry = {};
RectangleGeometry.create = function( ctr,  width, height, material )
{
	var x = ctr[0],  y = ctr[1],  z = 0.0;
	var	hw = 0.5*width,  hh = 0.5*height;

	// define some local variables
	this.vertices	= [];
	this.normals	= [];
	this.uvs		= [];
	this.indices	= [];

	// create the 4 vertices
	var nVertices = 4;
	RectangleGeometry.pushVertex( x-hw, y+hh, z);
	RectangleGeometry.pushVertex( x-hw, y-hh, z);
	RectangleGeometry.pushVertex( x+hw, y-hh, z);
	RectangleGeometry.pushVertex( x+hw, y+hh, z);

	// create the uv values for each vertex
	RectangleGeometry.pushUV( [0, 0] );
	RectangleGeometry.pushUV( [0, 1] );
	RectangleGeometry.pushUV( [1, 1] );
	RectangleGeometry.pushUV( [1, 0] );


	// create the per-vertex normals
	var n = [0, 0, 1];
	RectangleGeometry.pushNormal( n );
	RectangleGeometry.pushNormal( n );
	RectangleGeometry.pushNormal( n );
	RectangleGeometry.pushNormal( n );

	// create the 2 triangles
//	RectangleGeometry.pushIndices( 0, 1, 2 );
//	RectangleGeometry.pushIndices( 2, 3, 0 );
	RectangleGeometry.pushIndices( 2, 1, 0 );
	RectangleGeometry.pushIndices( 0, 3, 2 );

	//refine the mesh for vertex deformations
//	if (material)
//	{
//		if (material.hasVertexDeformation())
//		{
//			var paramRange = material.getVertexDeformationRange();
//			var tolerance = material.getVertexDeformationTolerance();
//			nVertices = ShapePrimitive.refineMesh( this.vertices, this.normals, this.uvs, this.indices, nVertices,  paramRange,  tolerance );
//		}
//	}

	// create the RDGE primitive
	var prim = ShapePrimitive.create(this.vertices, this.normals, this.uvs, this.indices, g_Engine.getContext().renderer.TRIANGLES, nVertices);
	return prim;
}

RectangleGeometry.pushVertex	= RectangleFill.pushVertex;
RectangleGeometry.pushNormal	= RectangleFill.pushNormal;
RectangleGeometry.pushUV		= RectangleFill.pushUV;
RectangleGeometry.pushIndices	= RectangleFill.pushIndices;
RectangleGeometry.getVertex		= RectangleFill.getVertex;
RectangleGeometry.getUV			= RectangleFill.getUV;



// Helper function for generating a RDGE primitive
ShapePrimitive = {};

ShapePrimitive.create = function(coords,  normals,  uvs,  indices, primType, vertexCount)
{
	var renderer = g_Engine.getContext().renderer;

	// to setup a primitive you must define it
	// create a new primitive definition here to then fill out
	var prim = new rdgePrimitiveDefinition();

	// the vertex definition declares how the data will be delivered to the shader
	// the position of an element in array determines which attribute in a shader the
	// data is bound to
	prim.vertexDefinition=
	{
		// this shows two ways to map this data to an attribute
		"vert":{'type':renderer.VS_ELEMENT_POS, 'bufferIndex':0, 'bufferUsage': renderer.BUFFER_STATIC},
		"a_pos":{'type':renderer.VS_ELEMENT_POS, 'bufferIndex':0, 'bufferUsage': renderer.BUFFER_STATIC},

		"normal":{'type':renderer.VS_ELEMENT_FLOAT3, 'bufferIndex':1, 'bufferUsage': renderer.BUFFER_STATIC},
		"a_nrm":{'type':renderer.VS_ELEMENT_FLOAT3, 'bufferIndex':1, 'bufferUsage': renderer.BUFFER_STATIC},
		"a_normal":{'type':renderer.VS_ELEMENT_FLOAT3, 'bufferIndex':1, 'bufferUsage': renderer.BUFFER_STATIC},

		"texcoord":{'type':renderer.VS_ELEMENT_FLOAT2, 'bufferIndex':2, 'bufferUsage': renderer.BUFFER_STATIC},
		"a_texcoord":{'type':renderer.VS_ELEMENT_FLOAT2, 'bufferIndex':2, 'bufferUsage': renderer.BUFFER_STATIC}
	};


	// the actual data that correlates to the vertex definition
	prim.bufferStreams=
	[
		coords,
		normals,
		uvs
	];

	// what type of buffers the data resides in, static is the most common case
	prim.streamUsage=
	[
		renderer.BUFFER_STATIC,
		renderer.BUFFER_STATIC,
		renderer.BUFFER_STATIC
	];

	// this tells the renderer to draw the primitive as a list of triangles
	prim.type = primType;

	prim.indexUsage = renderer.BUFFER_STREAM;
	prim.indexBuffer = indices;

	// finally the primitive is created, buffers are generated and the system determines
	// the data it needs to draw this primitive according to the previous definition
	renderer.createPrimitive(prim, vertexCount);

	return prim;
}