API Docs for: 0.9.16
Show:

File: phoria-util.js

/**
 * @fileoverview phoria - Utilities and helpers, including root namespace.
 * Polar/planer coordinate conversions and and polygon/line intersection methods - contribution from Ruan Moolman.
 * @author Kevin Roast
 * @date 10th April 2013
 * 
 * @module Util
 */

// init glMatrix library - many small Arrays are faster without the use of Float32Array wrap/conversion
glMatrix.setMatrixArrayType(Array);

/**
 * Creates a new vec3 initialized with the given xyz tuple
 *
 * @param {x:0,y:0,z:0} xyz object property tuple
 * @returns {vec3} a new 3D vector
 */
vec3.fromXYZ = function(xyz) {
   var out = new Array(3);
   out[0] = xyz.x;
   out[1] = xyz.y;
   out[2] = xyz.z;
   return out;
};

/**
 * Creates a new xyz object initialized with the given vec3 values
 *
 * @param {vec3} 3D vector
 * @returns {x:0,y:0,z:0} a new xyz object property tuple
 */
vec3.toXYZ = function(vec) {
   return {x:vec[0], y:vec[1], z:vec[2]};
};

/**
 * Creates a new vec4 initialized with the given xyz tuple and w coordinate
 *
 * @param {x:0,y:0,z:0} xyz object property tuple
 * @param w {Number} w coordinate
 * @returns {vec4} a new 4D vector
 */
vec4.fromXYZ = function(xyz, w) {
   var out = new Array(4);
   out[0] = xyz.x;
   out[1] = xyz.y;
   out[2] = xyz.z;
   out[3] = w;
   return out;
};

/**
 * Creates a rotation matrix from the given yaw (heading), pitch (elevation) and roll (bank) Euler angles.
 * 
 * @param {mat4} out the receiving matrix
 * @param {mat4} a the matrix to rotate
 * @param {Number} yaw the yaw/heading angle in radians
 * @param {Number} pitch the pitch/elevation angle in radians
 * @param {Number} roll the roll/bank angle in radians
 * @returns {mat4} out
 */
mat4.fromYPR = function(yaw, pitch, roll) {
   var out = new Array(16);
   var angles0 = Math.sin(roll),
       angles1 = Math.cos(roll),
       angles2 = Math.sin(pitch),
       angles3 = Math.cos(pitch),
       angles4 = Math.sin(yaw),
       angles5 = Math.cos(yaw);
   
   out[0] = angles5 * angles1;
   out[4] = -(angles5 * angles0);
   out[8] = angles4;
   out[1] = (angles2 * angles4 * angles1) + (angles3 * angles0);
   out[5] = (angles3 * angles1) - (angles2 * angles4 * angles0);
   out[9] = -(angles2 * angles5);
   out[2] = (angles2 * angles0) - (angles3 * angles4 * angles1);
   out[6] = (angles2 * angles1) + (angles3 * angles4 * angles0);
   out[10] = angles3 * angles5;
   out[3] = 0;
   out[7] = 0;
   out[11] = 0;
   out[12] = 0;
   out[13] = 0;
   out[14] = 0;
   out[15] = 1;
   return out;
};

quat.fromYPR = function(yaw, pitch, roll) {
    var num9 = roll * 0.5;
    var num6 = Math.sin(num9);
    var num5 = Math.cos(num9);
    var num8 = pitch * 0.5;
    var num4 = Math.sin(num8);
    var num3 = Math.cos(num8);
    var num7 = yaw * 0.5;
    var num2 = Math.sin(num7);
    var num = Math.cos(num7);
    var out = new Array(4);
    out[0] = ((num * num4) * num5) + ((num2 * num3) * num6);
    out[1] = ((num2 * num3) * num5) - ((num * num4) * num6);
    out[2] = ((num * num3) * num6) - ((num2 * num4) * num5);
    out[3] = ((num * num3) * num5) + ((num2 * num4) * num6);
    return out;
};


/**
 * Phoria root namespace.
 */
if (typeof Phoria === "undefined" || !Phoria)
{
   var Phoria = {};
   
   // Global static Phoria constants
   Phoria.RADIANS = Math.PI/180.0;
   Phoria.TWOPI = Math.PI*2;
   Phoria.ONEOPI = 1.0/Math.PI;
   Phoria.PIO2 = Math.PI/2;
   Phoria.PIO4 = Math.PI/4;
   Phoria.EPSILON = 0.000001;
}

/**
 * @class Util
 */
(function() {
   "use strict";

   Phoria.Util = {};
   
   /**
    * Utility to set up the prototype, constructor and superclass properties to
    * support an inheritance strategy that can chain constructors and methods.
    * Static members will not be inherited.
    *
    * @method extend
    * @param {Function} subc   the object to modify
    * @param {Function} superc the object to inherit
    * @param {Object} overrides  additional properties/methods to add to the
    *                            subclass prototype.  These will override the
    *                            matching items obtained from the superclass.
    */
   Phoria.Util.extend = function extend(subc, superc, overrides)
   {
      var F = function() {}, i;
      F.prototype = superc.prototype;
      subc.prototype = new F();
      subc.prototype.constructor = subc;
      subc.superclass = superc.prototype;
      if (superc.prototype.constructor == Object.prototype.constructor)
      {
         superc.prototype.constructor = superc;
      }
      
      if (overrides)
      {
         for (i in overrides)
         {
            if (overrides.hasOwnProperty(i))
            {
               subc.prototype[i] = overrides[i];
            }
         }
      }
   }
   
   /**
    * Augment an existing object prototype with additional properties and functions from another prototype.
    *
    * @method augment
    * @param {Object} r    Receiving object
    * @param {Object} s    Source object
    */
   Phoria.Util.augment = function augment(r, s)
   {
      for (var p in s.prototype)
      {
         if (typeof r.prototype[p] === "undefined")
         {
            r.prototype[p] = s.prototype[p];
         }
      }
   }
   
   /**
    * Merge two objects into a new object - does not affect either of the original objects.
    * Useful for Entity config default and user config merging.
    * Deep merge returning a combined object. The source overwrites the target if names match.
    * Nested Arrays contents including objects are also merged, source values for base datatypes win.
    *
    * @method merge
    */
   Phoria.Util.merge = function merge(target, src)
   {
      var array = Array.isArray(src),
          dst = array && [] || {};
      
      if (array)
      {
         target = target || [];
         dst = dst.concat(target);
         src.forEach(function(e, i)
         {
            if (typeof e === 'object')
            {
               dst[i] = Phoria.Util.merge(target[i], e);
            }
            else
            {
               // overwrite basic value types - source wins
               dst[i] = e;
            }
         });
      }
      else
      {
         if (target && typeof target === 'object')
         {
            Object.keys(target).forEach(function (key) {
               dst[key] = target[key];
            });
         }
         Object.keys(src).forEach(function (key) {
            if (typeof src[key] !== 'object' || !src[key])
            {
               dst[key] = src[key];
            }
            else
            {
               if (!target || !target[key])
               {
                  dst[key] = src[key];
               }
               else
               {
                  dst[key] = Phoria.Util.merge(target[key], src[key]);
               }
            }
         });
      }
      
      return dst;
   }

   /**
    * Deep combine a source object properties into a target object.
    * Like the merge function above, this will deep combine object and Arrays and the contents,
    * however it will overwrite the properties of the target when doing so.
    *
    * @method combine
    */
   Phoria.Util.combine = function combine(target, src)
   {
      var array = Array.isArray(src) && Array.isArray(target);
      if (array)
      {
         if (target.length < src.length) target.length = src.length
         src.forEach(function(e, i)
         {
            if (typeof e === 'object')
            {
               target[i] = target[i] || {};
               Phoria.Util.combine(target[i], e);
            }
            else
            {
               // overwrite basic value types - source wins
               target[i] = e;
            }
         });
      }
      else
      {
         Object.keys(src).forEach(function (key) {
            if (typeof src[key] !== 'object' || !src[key])
            {
               target[key] = src[key];
            }
            else
            {
               target[key] = target[key] || (Array.isArray(src[key]) ? [] : {});
               Phoria.Util.combine(target[key], src[key]);
            }
         });
      }
   }
   
   /**
    * Shallow and cheap (1 level deep only) clone for simple property based objects.
    * Properties are only safely copied if they are base datatypes or an array of such.
    * Should only be used for simple structures such as entity "style" objects.
    *
    * @method clone
    */
   Phoria.Util.clone = function clone(src)
   {
      var n = null,
          dst = {};
      for (var p in src)
      {
         n = src[p];
         if (Array.isArray(n))
         {
            dst[p] = [].concat(n);
         }
         else
         {
            dst[p] = n;
         }
      }
      return dst;
   }
   
   /**
    * Return true if the given mat4 is an identity (noop) matrix, false otherwise
    *
    * @method isIdentity
    * @papram mat   matrix to check
    *
    * @return {boolean}
    */
   Phoria.Util.isIdentity = function isIdentity(mat)
   {
      return (
         mat[0] === 1 && 
         mat[1] === 0 &&
         mat[2] === 0 &&
         mat[3] === 0 &&
         mat[4] === 0 &&
         mat[5] === 1 &&
         mat[6] === 0 &&
         mat[7] === 0 &&
         mat[8] === 0 &&
         mat[9] === 0 &&
         mat[10] === 1 &&
         mat[11] === 0 &&
         mat[12] === 0 &&
         mat[13] === 0 &&
         mat[14] === 0 &&
         mat[15] === 1);
   }

   /**
    * Calculate a vec4 normal vector from given tri coordinates
    *
    * @method calcNormalVector
    * @param x1
    * @param y1
    * @param z1
    * @param x2
    * @param y2
    * @param z2
    *
    * @return normalized vector
    */
   Phoria.Util.calcNormalVector = function calcNormalVector(x1, y1, z1, x2, y2, z2)
   {
      var v = vec4.fromValues(
         (y1 * z2) - (z1 * y2),
         -((z2 * x1) - (x2 * z1)),
         (x1 * y2) - (y1 * x2),
         0);
      // use vec3 here to save a pointless multiply * 0 and add op
      return vec3.normalize(v, v);
   }
   
   /**
    * Calculate the angle between two 3D vectors
    *
    * @method thetaTo
    * @param v1 first vector
    * @param v2 second vector
    *
    * @return {float} angle between two 3D vectors
    */
   Phoria.Util.thetaTo = function thetaTo(v1, v2)
   {
      return Math.acos(vec3.dot(v1, v2) / (Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2]) * Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2])));
   }
   
   /**
    * Return a vec3 representing the average world coordinate for the given polygon vertices
    * @method averagePolyVertex
    */
   Phoria.Util.averagePolyVertex = function averagePolyVertex(vertices, worldcoords)
   {
      for (var i=0,avx=0,avy=0,avz=0; i<vertices.length; i++)
      {
         avx += worldcoords[ vertices[i] ][0];
         avy += worldcoords[ vertices[i] ][1];
         avz += worldcoords[ vertices[i] ][2];
      }
      return vec3.fromValues(
         avx / vertices.length,
         avy / vertices.length,
         avz / vertices.length);
   }
   
   /**
    * Return the average Z coordinate for a list of coordinates
    *
    * @method averageObjectZ
    */
   Phoria.Util.averageObjectZ = function averageObjectZ(coords)
   {
      var av = 0;
      for (var i=0; i<coords.length; i++)
      {
         av += coords[i][3];
      }
      return av / coords.length;
   }

   /**
    * Return an Array of a given length using the given factory function to populate each item
    *
    * @method populateBuffer
    * @param len {int}   length
    * @param fnFactory   function factory
    *
    * @return {Array}
    */
   Phoria.Util.populateBuffer = function populateBuffer(len, fnFactory)
   {
      var array = new Array(len);
      for (var i=0; i<len; i++)
      {
         array[i] = fnFactory(i);
      }
      return array;
   }

   /**
    * Sort a list of polygons by the Z coordinates in the supplied coordinate list
    *
    * @method sortPolygons
    * @param polygons {Array} list of polygons
    * @param worldcoords {
    */
   Phoria.Util.sortPolygons = function sortPolygons(polygons, worldcoords)
   {
      for (var i=0,verts; i<polygons.length; i++)
      {
         verts = polygons[i].vertices;
         if (verts.length === 3)
         {
            polygons[i]._avz = (worldcoords[ verts[0] ][2] + worldcoords[ verts[1] ][2] + worldcoords[ verts[2] ][2]) * 0.333333;
         }
         else
         {
            polygons[i]._avz = (worldcoords[ verts[0] ][2] + worldcoords[ verts[1] ][2] + worldcoords[ verts[2] ][2] + worldcoords[ verts[3] ][2]) * 0.25;
         }
      }
      polygons.sort(function sortPolygonsZ(f1, f2) {
         return (f1._avz < f2._avz ? -1 : 1);
      });
   }

   /**
    * Sort a list of edges by the average Z coordinate of the two vertices that represent it each edge.
    *
    * @method sortEdges
    */
   Phoria.Util.sortEdges = function sortEdges(edges, coords)
   {
      for (var i=0; i<edges.length; i++)
      {
         edges[i]._avz = (coords[ edges[i].a ][2] + coords[ edges[i].b ][2]) * 0.5;
      }
      edges.sort(function sortEdgesZ(f1, f2) {
         return (f1._avz < f2._avz ? -1 : 1);
      });
   }

   /**
    * Sort a list of points by the Z coordinate. A second list is supplied that will be sorted in
    * lock-step with the first list (to maintain screen and worldcoordinate list)
    *
    * @method sortPoints
    */
   Phoria.Util.sortPoints = function sortPoints(coords, worldcoords)
   {
      // We need our own sort routine as we need to swap items within two lists during the sorting, as
      // they must be maintained in lock-step or the lighting processing (using matching worldcoord indexes)
      // will produce incorrect results
      var quickSort = function qSort(c, a, start, end) {
         if (start < end) {
            var pivotIndex = (start + end) >> 1,
                pivotValue = a[pivotIndex][2],
                pivotIndexNew = start;
            
            var tmp = a[pivotIndex];
            a[pivotIndex] = a[end];
            a[end] = tmp;
            tmp = c[pivotIndex];
            c[pivotIndex] = c[end];
            c[end] = tmp;
            
            for (var i = start; i < end; i++)
            {
               if (a[i][2] > pivotValue)
               {
                  tmp = c[i];
                  c[i] = c[pivotIndexNew];
                  c[pivotIndexNew] = tmp;
                  tmp = a[i];
                  a[i] = a[pivotIndexNew];
                  a[pivotIndexNew] = tmp;
                  
                  pivotIndexNew++;
               }
            }
            
            tmp = c[pivotIndexNew];
            c[pivotIndexNew] = c[end];
            c[end] = tmp;
            tmp = a[pivotIndexNew];
            a[pivotIndexNew] = a[end];
            a[end] = tmp;
            
            qSort(c, a, start, pivotIndexNew-1);
            qSort(c, a, pivotIndexNew+1, end);
         }
      };
      quickSort(worldcoords, coords, 0, coords.length - 1);
   }
   
   /**
    * Generates an object of a subdivided plane 0-1 in the x-z plane
    *
    * @method generateTesselatedPlane
    * @param vsegs   Number of vertical segments
    * @param hsegs   Number of horizontal segments
    * @param level   TODO: Subdivision level, 0-2 (quads, 2 tris, 4 tris)
    * @param scale   Scale of the plane - 1.0 is a unit plane centred on the origin
    */
   Phoria.Util.generateTesselatedPlane = function generateTesselatedPlane(vsegs, hsegs, level, scale, generateUVs)
   {
      var points = [], edges = [], polys = [],
          hinc = scale/hsegs, vinc = scale/vsegs, c = 0;
      for (var i=0, x, y = scale/2; i<=vsegs; i++)
      {
         x = -scale/2;
         for (var j=0; j<=hsegs; j++)
         {
            // generate a row of points
            points.push( {x: x, y: 0, z: y} );
            
            // edges
            if (j !== 0)
            {
               edges.push( {a:c, b:c-1} );
            }
            if (i !== 0)
            {
               edges.push( {a:c, b:c-hsegs-1} );
            }

            if (i !== 0 && j !== 0)
            {
               // generate quad
               var p = {vertices:[c-hsegs-1, c, c-1, c-hsegs-2]};
               if (generateUVs)
               {
                  var uvs = [(1/hsegs) * j, (1/vsegs) * (i-1),
                             (1/hsegs) * j, (1/vsegs) * i,
                             (1/hsegs) * (j-1), (1/vsegs) * i,
                             (1/hsegs) * (j-1), (1/vsegs) * (i-1)];
                  p.uvs = uvs;
               }
               polys.push(p);
            }
            
            x += hinc;
            c++;
         }
         y -= vinc;
      }
      
      return {
         points: points,
         edges: edges,
         polygons: polys
      };
   }

   /**
    * Generate the geometry for a 1x1x1 unit cube
    *
    * @method generateUnitCube
    * @param scale   optional scaling factor
    */
   Phoria.Util.generateUnitCube = function generateUnitCube(scale)
   {
      var s = scale || 1;
      return {
         points: [{x:-1*s,y:1*s,z:-1*s}, {x:1*s,y:1*s,z:-1*s}, {x:1*s,y:-1*s,z:-1*s}, {x:-1*s,y:-1*s,z:-1*s},
                  {x:-1*s,y:1*s,z:1*s}, {x:1*s,y:1*s,z:1*s}, {x:1*s,y:-1*s,z:1*s}, {x:-1*s,y:-1*s,z:1*s}],
         edges: [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
         polygons: [{vertices:[0,1,2,3]},{vertices:[1,5,6,2]},{vertices:[5,4,7,6]},{vertices:[4,0,3,7]},{vertices:[4,5,1,0]},{vertices:[3,2,6,7]}]
      };
   }

   /**
    * Generate the geometry for 1x1.5x1 unit square based pyramid
    *
    * @method generatePyramid
    * @param scale   optional scaling factor
    */
   Phoria.Util.generatePyramid = function generatePyramid(scale)
   {
      var s = scale || 1;
      return {
         points: [{x:-1*s,y:0,z:-1*s}, {x:-1*s,y:0,z:1*s}, {x:1*s,y:0,z:1*s}, {x:1*s,y:0*s,z:-1*s}, {x:0,y:1.5*s,z:0}],
         edges: [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}],
         polygons: [{vertices:[0,1,4]},{vertices:[1,2,4]},{vertices:[2,3,4]},{vertices:[3,0,4]},{vertices:[3,2,1,0]}]
      };
   }

   /**
    * Generate the geometry for a unit Icosahedron
    *
    * @method generateIcosahedron
    * @param scale   optional scaling factor
    */
   Phoria.Util.generateIcosahedron = function generateIcosahedron(scale)
   {
      // Generator code from "Tessellation of sphere" http://student.ulb.ac.be/~claugero/sphere/index.html
      var s = scale || 1;
      var t = (1+Math.sqrt(5))/2,
          tau = (t/Math.sqrt(1+t*t)) * s,
          one = (1/Math.sqrt(1+t*t)) * s;
      return {
         points: [{x:tau,y:one,z:0}, {x:-tau,y:one,z:0}, {x:-tau,y:-one,z:0}, {x:tau,y:-one,z:0}, {x:one,y:0,z:tau}, {x:one,y:0,z:-tau}, {x:-one,y:0,z:-tau}, {x:-one,y:0,z:tau}, {x:0,y:tau,z:one}, {x:0,y:-tau,z:one}, {x:0,y:-tau,z:-one}, {x:0,y:tau,z:-one}],
         edges: [{a:4,b:8}, {a:8,b:7}, {a:7,b:4}, {a:7,b:9}, {a:9,b:4}, {a:5,b:6}, {a:6,b:11}, {a:11,b:5}, {a:5,b:10}, {a:10,b:6}, {a:0,b:4}, {a:4,b:3}, {a:3,b:0}, {a:3,b:5}, {a:5,b:0}, {a:2,b:7}, {a:7,b:1}, {a:1,b:2}, {a:1,b:6}, {a:6,b:2}, {a:8,b:0}, {a:0,b:11}, {a:11,b:8}, {a:11,b:1}, {a:1,b:8}, {a:9,b:10}, {a:10,b:3}, {a:3,b:9}, {a:9,b:2}, {a:2,b:10}],
         polygons: [{vertices:[4, 8, 7]}, {vertices:[4, 7, 9]}, {vertices:[5, 6, 11]}, {vertices:[5, 10, 6]}, {vertices:[0, 4, 3]}, {vertices:[0, 3, 5]}, {vertices:[2, 7, 1]}, {vertices:[2, 1, 6]}, {vertices:[8, 0, 11]}, {vertices:[8, 11, 1]}, {vertices:[9, 10, 3]}, {vertices:[9, 2, 10]}, {vertices:[8, 4, 0]}, {vertices:[11, 0, 5]}, {vertices:[4, 9, 3]}, {vertices:[5, 3, 10]}, {vertices:[7, 8, 1]}, {vertices:[6, 1, 11]}, {vertices:[7, 2, 9]}, {vertices:[6, 10, 2]}]
      };
   }
   
   /**
    * Subdivide the given vertices and triangles - using a basic normalised triangle subdivision algorithm.
    * From OpenGL tutorial chapter "Subdividing to Improve a Polygonal Approximation to a Surface".
    * NOTE: this only works on triangles or quads not higher order polygons.
    * 
    * TODO: currently this subdivide does not reuse vertices that are shared by polygons!
    *
    * @method subdivide
    */
   Phoria.Util.subdivide = function subdivide(v, p)
   {
      var vertices = [],
          polys = [];
      
      var fnNormalize = function(vn) {
         var len = vn.x*vn.x + vn.y*vn.y + vn.z*vn.z;
         len = 1 / Math.sqrt(len);
         vn.x *= len;
         vn.y *= len;
         vn.z *= len;
      }
      var fnSubDivide = function(v1, v2, v3) {
         var v12 = {x:0,y:0,z:0}, v23 = {x:0,y:0,z:0}, v31 = {x:0,y:0,z:0};
         
         v12.x = v1.x+v2.x; v12.y = v1.y+v2.y; v12.z = v1.z+v2.z;
         v23.x = v2.x+v3.x; v23.y = v2.y+v3.y; v23.z = v2.z+v3.z;
         v31.x = v3.x+v1.x; v31.y = v3.y+v1.y; v31.z = v3.z+v1.z;
         
         fnNormalize(v12);
         fnNormalize(v23);
         fnNormalize(v31);
         
         var pn = vertices.length;
         vertices.push(v1,v2,v3,v12,v23,v31);
         polys.push({vertices: [pn+0, pn+3, pn+5]});
         polys.push({vertices: [pn+1, pn+4, pn+3]});
         polys.push({vertices: [pn+2, pn+5, pn+4]});
         polys.push({vertices: [pn+3, pn+4, pn+5]});
      }
      for (var i=0,vs; i<p.length; i++)
      {
         vs = p[i].vertices;
         if (vs.length === 3)
         {
            fnSubDivide.call(this, v[vs[0]], v[vs[1]], v[vs[2]]);
         }
         else if (vs.length === 4)
         {
            fnSubDivide.call(this, v[vs[0]], v[vs[1]], v[vs[2]]);
            fnSubDivide.call(this, v[vs[2]], v[vs[3]], v[vs[0]]);
         }
      }
      
      return {
         points: vertices,
         polygons: polys
      };
   }
   
   /**
    * Generate geometry for a cylinder
    *
    * @method generateCylinder
    * @param radius  Radius of the cylinder
    * @param length  Length of the cylinder
    * @param strips  Number of strips around the cylinder
    */
   Phoria.Util.generateCylinder = function generateCylinder(radius, length, strips)
   {
      var points = [], polygons = [], edges = [];
      var inc = 2*Math.PI / strips;
      for (var s=0, offset=0; s<=strips; s++)
      {
         points.push({
            x: Math.cos(offset) * radius,
            z: Math.sin(offset) * radius,
            y: length/2
         });
         points.push({
            x: Math.cos(offset) * radius,
            z: Math.sin(offset) * radius,
            y: -length/2
         });
         offset += inc;
         if (s !== 0)
         {
            // quad strip
            polygons.push({vertices: [s*2-2, s*2, s*2+1, s*2-1]});
            // edges
            edges.push({a:s*2, b:s*2-2},{a:s*2-2,b:s*2-1},{a:s*2+1,b:s*2-1});
            if (s === strips - 1)
            {
               // end cap polygons
               var vs = [];
               for (var i=strips; i>=0; i--) vs.push(i*2);
               polygons.push({vertices: vs});
               vs = [];
               for (var i=0; i<strips; i++) vs.push(i*2+1);
               polygons.push({vertices: vs});
            }
         }
      }
      return {
         points: points,
         edges: edges,
         polygons: polygons
      };
   }

   /**
    * <pre>
    * desc = {
    *    scalex: 1,
    *    scaley: 1,
    *    scalez: 1,
    *    offsetx: 0,
    *    offsety: 0,
    *    offsetz: 0
    * }
    * </pre>
    *
    * @method generateCuboid
    */
   Phoria.Util.generateCuboid = function generateCuboid(desc)
   {
      var scalex = desc.scalex || 1,
          scaley = desc.scaley || 1,
          scalez = desc.scalez || 1,
          offsetx = desc.offsetx || 0,
          offsety = desc.offsety || 0,
          offsetz = desc.offsetz || 0;
      return {
         points: [{x:-1*scalex,y:1*scaley,z:-1*scalez}, {x:1*scalex,y:1*scaley,z:-1*scalez}, {x:1*scalex,y:-1*scaley,z:-1*scalez}, {x:-1*scalex,y:-1*scaley,z:-1*scalez},
                  {x:-1*scalex,y:1*scaley,z:1*scalez}, {x:1*scalex,y:1*scaley,z:1*scalez}, {x:1*scalex,y:-1*scaley,z:1*scalez}, {x:-1*scalex,y:-1*scaley,z:1*scalez}],
         edges: [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
         polygons: [{vertices:[0,1,2,3]},{vertices:[0,4,5,1]},{vertices:[1,5,6,2]},{vertices:[2,6,7,3]},{vertices:[4,0,3,7]},{vertices:[5,4,7,6]}]
      };
   }

   /**
    * Generate the geometry for a sphere - triangles form the top and bottom segments, quads form the strips.
    *
    * @method generateSphere
    */
   Phoria.Util.generateSphere = function generateSphere(scale, lats, longs, generateUVs)
   {
      var points = [], edges = [], polys = [], uvs = [];

      for (var latNumber = 0; latNumber <= lats; ++latNumber)
      {
         for (var longNumber = 0; longNumber <= longs; ++longNumber)
         {
            var theta = latNumber * Math.PI / lats;
            var phi = longNumber * 2 * Math.PI / longs;
            var sinTheta = Math.sin(theta);
            var sinPhi = Math.sin(phi);
            var cosTheta = Math.cos(theta);
            var cosPhi = Math.cos(phi);

            var x = cosPhi * sinTheta;
            var y = cosTheta;
            var z = sinPhi * sinTheta;
            if (generateUVs)
            {
               var u = longNumber/longs;
               var v = latNumber/lats;
               uvs.push({u: u, v: v});
            }
            points.push({
               x: scale * x,
               y: scale * y,
               z: scale * z});
         }
      }

      for (var latNumber = 0; latNumber < lats; ++latNumber)
      {
         for (var longNumber = 0; longNumber < longs; ++longNumber)
         {
            var first = (latNumber * (longs+1)) + longNumber;
            var second = first + longs + 1;
            
            if (latNumber === 0)
            {
               // top triangle
               var p = {vertices: [first+1, second+1, second]};
               if (generateUVs)
               {
                  p.uvs = [uvs[first+1].u, uvs[first+1].v, uvs[second+1].u, uvs[second+1].v, uvs[second].u, uvs[second].v]
               }
               polys.push(p);
               edges.push({a:first, b:second});
            }
            else if (latNumber === lats-1)
            {
               // bottom triangle
               var p = {vertices: [first+1, second, first]};
               if (generateUVs)
               {
                  p.uvs = [uvs[first+1].u, uvs[first+1].v, uvs[second].u, uvs[second].v, uvs[first].u, uvs[first].v]
               }
               polys.push(p);
               edges.push({a:first, b:second});
            }
            else
            {
               // quad strip
               var p = {vertices: [first+1, second+1, second, first]};
               if (generateUVs)
               {
                  p.uvs = [uvs[first+1].u, uvs[first+1].v, uvs[second+1].u, uvs[second+1].v, uvs[second].u, uvs[second].v, uvs[first].u, uvs[first].v]
               }
               polys.push(p);
               edges.push({a:first, b:second});
               edges.push({a:second, b:second+1});
            }
         }
      }

      return {
         points: points,
         edges: edges,
         polygons: polys
      };
   }

   /**
    * Generate an Image for a radial gradient, with the given inner and outer colour stops.
    * Useful to generate quick sprite images of blurred spheres for explosions, particles etc.
    *
    * @method generateRadialGradientBitmap
    */
   Phoria.Util.generateRadialGradientBitmap = function generateRadialGradientBitmap(size, innerColour, outerColour)
   {
      var buffer = document.createElement('canvas'),
          width = size << 1;
      buffer.width = buffer.height = width;
      var ctx = buffer.getContext('2d'),
          radgrad = ctx.createRadialGradient(size, size, size >> 1, size, size, size);  
      radgrad.addColorStop(0, innerColour);
      radgrad.addColorStop(1, outerColour);
      ctx.fillStyle = radgrad;
      ctx.fillRect(0, 0, width, width);
      var img = new Image();
      img.src = buffer.toDataURL("image/png");
      return img;
   }
   
   /**
    * Make an XHR request for a resource. E.g. for loading a 3D object file format or similar.
    *
    * @method request
    * @param config  JavaScript object describing the url, method, callback and so on for the request:
    * <pre>
    *     {
    *       url: url                      // url of resource (mandatory)
    *       method: "GET"                 // HTTP method - default is GET
    *       overrideMimeType: mimetype    // optional mimetype override for response stream
    *       requestContentType: mimetype  // optional request Accept content-type
    *       fnSuccess: function           // success handler function - function(responseText, responseJSON)
    *       fnFailure: function           // failure handler function - function(responseText, responseJSON)
    *       data: string                  // data for POST or PUT method
    *    }
    * </pre>
    */
   Phoria.Util.request = function request(config)
   {
      var req = new XMLHttpRequest();
      var data = config.data || "";
      if (config.responseContentType && req.overrideMimeType) req.overrideMimeType(config.responseContentType);
      req.open(config.method ? config.method : "GET", config.url);
      if (config.requestContentType) req.setRequestHeader("Accept", config.requestContentType);
      req.onreadystatechange = function() {
         if (req.readyState === 4)
         {
            if (req.status === 200)
            {
               // success - call handler
               if (config.fnSuccess)
               {
                  config.fnSuccess.call(this, req.responseText, req.status);
               }
            }
            else
            {
               // failure - call handler
               if (config.fnFailure)
               {
                  config.fnFailure.call(this, req.responseText, req.status);
               }
               else
               {
                  // default error handler
                  alert(req.status + "\n\n" + req.responseText);
               }
            }
         }
      };
      try
      {
         if (config.method === "POST" || config.method === "PUT")
         {
            req.send(data);
         }
         else
         {
            req.send(null);
         }
      }
      catch (e)
      {
         alert(e.message);
      }
   }
   
   /**
    * Geometry importer for Wavefront (.obj) text 3D file format. The url is loaded via an XHR
    * request and a callback function is executed on completion of the import and processing.
    *
    * @method importGeometryWavefront
    * @param config  JavaScript object describing the url and configuration params for the import:
    *    {
    *       url: url             // url of resource (mandatory)
    *       fnSuccess: function  // callback function to execute once object is loaded - function({points:[], polygons:[]})
    *       fnFailure: function  // optional callback function to execute if an error occurs
    *       scale: 1.0           // optional scaling factor - 1.0 is the default
    *       scaleTo: 1.0         // optional automatically scale object to a specific size
    *       center: false        // optional centering of imported geometry to the origin
    *       reorder: false       // true to switch order of poly vertices if back-to-front ordering
    *    }
    */
   Phoria.Util.importGeometryWavefront = function importGeometryWavefront(config)
   {
      var vertex = [], faces = [], uvs = [];
      var re = /\s+/;   // 1 or more spaces can separate tokens within a line
      var scale = config.scale || 1;
      var minx, miny, minz, maxx, maxy, maxz;
      minx = miny = minz = maxx = maxy = maxz = 0;
      
      Phoria.Util.request({
         url: config.url,
         fnSuccess: function(data) {
            var lines = data.split('\n'); // split line by line
            for (var i = 0;i < lines.length;i++)
            {
               var line = lines[i].split(re);
               
               switch (line[0])
               {
                  case 'v':
                  {
                     var x = parseFloat(line[1])*scale,
                         y = parseFloat(line[2])*scale,
                         z = parseFloat(line[3])*scale;
                     vertex.push({'x': x, 'y': y, 'z': z});
                     if (x < minx) minx = x;
                     else if (x > maxx) maxx = x;
                     if (y < miny) miny = y;
                     else if (y > maxy) maxy = y;
                     if (z < minz) minz = z;
                     else if (z > maxz) maxz = z;
                  }
                  break;
                  
                  case 'vt':
                  {
                     var u = parseFloat(line[1]),
                         v = parseFloat(line[2]);
                     uvs.push([u,v]);
                  }
                  break;
                  
                  case 'f':
                  {
                     line.splice(0, 1); // remove "f"
                     var vertices = [], uvcoords = [];
                     for (var j = 0,vindex,vps; j < line.length; j++)
                     {
                        vindex = line[config.reorder ? line.length - j - 1 : j];
                        // deal with /r/n line endings
                        if (vindex.length !== 0)
                        {
                           // OBJ format vertices are indexed from 1
                           vps = vindex.split('/');
                           vertices.push(parseInt(vps[0]) - 1);
                           // gather texture coords
                           if (vps.length > 1 && vindex.indexOf("//") === -1)
                           {
                              var uv = parseInt(vps[1]) - 1;
                              if (uvs.length > uv)
                              {
                                 uvcoords.push(uvs[uv][0], uvs[uv][1]);
                              }
                           }
                        }
                     }
                     var poly = {'vertices': vertices};
                     faces.push(poly);
                     if (uvcoords.length !== 0) poly.uvs = uvcoords;
                  }
                  break;
               }
            }
            if (config.center)
            {
               // calculate centre displacement for object and adjust each point
               var cdispx = (minx + maxx)/2.0,
                   cdispy = (miny + maxy)/2.0,
                   cdispz = (minz + maxz)/2.0;
               for (var i=0; i<vertex.length; i++)
               {
                  vertex[i].x -= cdispx;
                  vertex[i].y -= cdispy;
                  vertex[i].z -= cdispz;
               }
            }
            if (config.scaleTo)
            {
               // calc total size multipliers using max object limits and scale
               var sizex = maxx - minx,
                   sizey = maxy - miny,
                   sizez = maxz - minz;
         
               // find largest of multipliers and use it as scale factor
               var scalefactor = 0.0;
               if (sizey > sizex) 
               {
                  if (sizez > sizey) 
                  {
                     // use sizez
                     scalefactor = 1.0 / (sizez/config.scaleTo);
                  }
                  else
                  {
                     // use sizey
                     scalefactor = 1.0 / (sizey/config.scaleTo);
                  }
               }
               else if (sizez > sizex) 
               {
                  // use sizez
                  scalefactor = 1.0 / (sizez/config.scaleTo);
               }
               else 
               {
                  // use sizex
                  scalefactor = 1.0 / (sizex/config.scaleTo);
               }
               for (var i=0; i<vertex.length; i++)
               {
                  vertex[i].x *= scalefactor;
                  vertex[i].y *= scalefactor;
                  vertex[i].z *= scalefactor;
               }
            }
            if (config.fnSuccess)
            {
               config.fnSuccess.call(this, {
                  points: vertex,
                  polygons: faces
               });
            }
         },
         fnFailure: function(error) {
            if (config.fnFailure)
            {
               config.fnFailure.call(this, error);
            }
         }
      });
   }
   
   Phoria.Util.calculatePolarFromPlanar = function calculatePolarFromPlanar(planar)
   {
      // array positions correspond to: r = [0], t = [1], p = [2]
      var point = new vec3.create();
      // r is radius and equals the length of the planar vector
      point[0] = vec3.length(planar);
      // t is theta and represents the vertical angle from the z axis to the point
      point[1] = Math.acos(planar[2] / point[0]);
      // p is phi and represents the horizontal angle from the x axis to the point
      if (planar[0] !== 0)
      {
         if (planar[0] > 0)
            point[2] = Math.atan(planar[1] / planar[0]);
         else
            point[2] = Math.PI + Math.atan(planar[1] / planar[0]);
      }
      // if x = 0
      else
      {
         if (planar[1] > 0)
            point[2] = Math.PI / 2;
         else
            point[2] = Math.PI * 3 / 2;
      }
      return point;
   }

   Phoria.Util.calculatePlanarFromPolar = function calculatePlanarFromPolar(polar)
   {
      return new vec3.fromValues(
         // calculate x value from polar coordinates
         Math.round(polar[0] * Math.sin(polar[1]) * Math.cos(polar[2]) * 100) / 100,
         // calculate y value from polar coordinates
         Math.round(polar[0] * Math.sin(polar[1]) * Math.sin(polar[2]) * 100) / 100,
         // calculate z value from polar coordinates
         Math.round(polar[0] * Math.cos(polar[1]) * 100) / 100);
   }

   Phoria.Util.planeLineIntersection = function planeLineIntersection(planeNormal, planePoint, lineVector, linePoint)
   {
      // planeNormal . (plane - planePoint) = 0
      // line = linePoint + lineScalar * lineVector
      // intersect where line = plane, thus
      // planeNormal . (linePoint + lineScalar * lineVector - planePoint) = 0
      // giving: lineScalar = planeNormal . (planePoint - linePoint) / planeNormal . lineVector
      var dotProduct = vec3.dot(lineVector, planeNormal);
      // check that click vector is not parallel to polygon
      if (dotProduct !== 0)
      {
         var pointVector = new vec3.create();
         vec3.subtract(pointVector, planePoint, linePoint);
         var lineScalar = vec3.dot(planeNormal, pointVector) / dotProduct;
         var intersection = vec3.create();
         vec3.scaleAndAdd(intersection, linePoint, lineVector, lineScalar);
         return intersection;
      }
      else
      {
         // return null if parallel, as the vector will never intersect the plane
         return null;
      }
   }

   Phoria.Util.intersectionInsidePolygon = function intersectionInsidePolygon(polygon, points, intersection)
   {
      // get absolute values of polygons normal vector
      var absNormal = vec3.fromValues(Math.abs(polygon._worldnormal[0]), Math.abs(polygon._worldnormal[1]), Math.abs(polygon._worldnormal[2]));
      // intersection counter
      var numIntersects = 0;
      // the vector for the test line, can be any 2D vector
      var testVector = vec2.fromValues(1, 1);

      // for every vertice of the polygon
      for (var l = 0; l < polygon.vertices.length; l++)
      {
         var point1, point2,
             intersection2D;

         // use orthogonal planes to check if the point is in shape in 2D
         // the component with the highest normal value is dropped
         // as this gives the best approximation of the original shape

         // drop z coordinates
         if (absNormal[2] >= absNormal[0] && absNormal[2] >= absNormal[1])
         {
            point1 = vec2.fromValues(points[polygon.vertices[l]][0], points[polygon.vertices[l]][1]);
            point2;
            if (l < polygon.vertices.length - 1)
               point2 = vec2.fromValues(points[polygon.vertices[l + 1]][0], points[polygon.vertices[l + 1]][1]);
            else
               point2 = vec2.fromValues(points[polygon.vertices[0]][0], points[polygon.vertices[0]][1]);

            intersection2D = vec2.fromValues(intersection[0], intersection[1]);
         }
         // drop y coordinates
         else if (absNormal[1] > absNormal[0])
         {
            point1 = vec2.fromValues(points[polygon.vertices[l]][2], points[polygon.vertices[l]][0]);
            point2;
            if (l < polygon.vertices.length - 1)
               point2 = vec2.fromValues(points[polygon.vertices[l + 1]][2], points[polygon.vertices[l + 1]][0]);
            else
               point2 = vec2.fromValues(points[polygon.vertices[0]][2], points[polygon.vertices[0]][0]);

            intersection2D = vec2.fromValues(intersection[2], intersection[0]);
         }
         // drop x coordinates
         else
         {
            point1 = vec2.fromValues(points[polygon.vertices[l]][1], points[polygon.vertices[l]][2]);
            point2;
            if (l < polygon.vertices.length - 1)
               point2 = vec2.fromValues(points[polygon.vertices[l + 1]][1], points[polygon.vertices[l + 1]][2]);
            else
               point2 = vec2.fromValues(points[polygon.vertices[0]][1], points[polygon.vertices[0]][2]);

            intersection2D = vec2.fromValues(intersection[1], intersection[2]);
         }

         // check if the vector from the intersection point intersects the line section
         if (Phoria.Util.sectionLineIntersect2D(point1, point2, intersection2D, testVector))
         {
            // increase intersect counter
            numIntersects++;
         }
      }

      // uneven number of intersects, mean the point is inside the object
      // even number of intersects, means its outside
      return (numIntersects % 2 === 1);
   }

   Phoria.Util.sectionLineIntersect2D = function sectionLineIntersect2D(p1, p2, p, v)
   {
      // get line section's vector
      var s = vec2.create();
      vec2.subtract(s, p2, p1);

      // calculate cross product of line vectors
      var svCross = vec3.create();
      vec2.cross(svCross, s, v)

      // if lines are parallel, they will never intersect
      if (svCross[2] === 0)
         return false;

      // l1 = p1 + t * s
      // l2 = p + u * v
      // where l1 = l2 the lines intersect thus,
      // t = (p x v - p1 x v) / (s x v)
      var t = (p[0] * v[1] - p[1] * v[0] - p1[0] * v[1] + p1[1] * v[0]) / svCross[2];
      // if v's x value is 0, use the other equation to calculate scalar u.
      var u;
      if (v[0] !== 0)
         u = (p1[0] + t * s[0] - p[0]) / v[0];
      else
         u = (p1[1] + t * s[1] - p[1]) / v[1];

      // intersection point
      var ip = vec2.create();
      vec2.scaleAndAdd(ip, p1, s, t);

      // check if intersection is in the section line
      var doesIntersect = { x: false, y: false };

      // only check in positive direction of test vector
      if (u >= 0)
      {
         if (p1[0] > p2[0])
         {
            if (ip[0] <= p1[0] && ip[0] >= p2[0])
               doesIntersect.x = true;
         }
         else
         {
            if (ip[0] >= p1[0] && ip[0] <= p2[0])
               doesIntersect.x = true;
         }

         if (p1[1] > p2[1])
         {
            if (ip[1] <= p1[1] && ip[1] >= p2[1])
               doesIntersect.y = true;
         }
         else
         {
            if (ip[1] >= p1[1] && ip[1] <= p2[1])
               doesIntersect.y = true;
         }
      }
      // return true if it is
      return (doesIntersect.x && doesIntersect.y);
   }

})();


/**
 * Image Preloader class. Executes the supplied callback function once all
 * registered images are loaded by the browser.
 * 
 * @class Preloader
 */
(function() {
   "use strict";

   Phoria.Preloader = function()
   {
      this.images = [];
      return this;
   };
   
   Phoria.Preloader.prototype =
   {
      /**
       * Image list
       *
       * @property images
       * @type Array
       */
      images: null,
      
      /**
       * Callback function
       *
       * @property callback
       * @type Function
       */
      callback: null,
      
      /**
       * Images loaded so far counter
       */
      counter: 0,
      
      /**
       * Add an image to the list of images to wait for
       */
      addImage: function addImage(img, url)
      {
         var me = this;
         img.url = url;
         // attach closure to the image onload handler
         img.onload = function()
         {
            me.counter++;
            if (me.counter === me.images.length)
            {
               // all images are loaded - execute callback function
               me.callback.call(me);
            }
         };
         this.images.push(img);
      },
      
      /**
       * Load the images and call the supplied function when ready
       */
      onLoadCallback: function onLoadCallback(fn)
      {
         this.counter = 0;
         this.callback = fn;
         // load the images
         for (var i=0, j=this.images.length; i<j; i++)
         {
            this.images[i].src = this.images[i].url;
         }
      }
   };
})();