GLDraw.ts

import { Vector3 } from "./WebGL/math";
import { Geometry } from "./WebGL";
import { Color, Colored } from "colors";


//define enum values
/**
 * Enum for cylinder cap styles.
 * @readonly
 * @enum 
 * @property NONE
 * @property FLAT
 * @property ROUND
 */
export enum CAP {
    NONE = 0,
    FLAT = 1,
    ROUND = 2
};

export interface Point {
    x: number;
    y: number;
    z: number;
}

/**
 * Lower level utilities for creating WebGL shape geometries.
 * These are not intended for general consumption.
 * @namespace 
  */
export namespace GLDraw {

    // Rotation matrix around z and x axis -
    // according to y basis vector
    // TODO: Try to optimize this (square roots?)
    function getRotationMatrix(dx: number, dy: number, dz: number) {
        var dxy = Math.hypot(dx, dy);
        var dyz;

        var sinA, cosA, sinB, cosB;

        // about z axis - Phi
        if (dxy < 0.0001) {
            sinA = 0;
            cosA = 1;
        }

        else {
            sinA = -dx / dxy;
            cosA = dy / dxy;
        }

        // recast dy in terms of new axes - z is the same

        dy = -sinA * dx + cosA * dy;
        dyz = Math.hypot(dy, dz);

        // about new x axis - Theta

        if (dyz < 0.0001) {
            sinB = 0;
            cosB = 1;
        }

        else {
            sinB = dz / dyz;
            cosB = dy / dyz;
        }

        var rot = new Float32Array(9);
        rot[0] = cosA;
        rot[1] = sinA;
        rot[2] = 0;
        rot[3] = -sinA * cosB;
        rot[4] = cosA * cosB;
        rot[5] = sinB;
        rot[6] = sinA * sinB;
        rot[7] = -cosA * sinB;
        rot[8] = cosB;

        return rot;

    };


    // memoize capped cylinder for given radius cylVertexCache
    class CylVertexCache {

        // memoize both rounded and flat caps (hemisphere and circle)
        cache: any = {};


        // Ortho normal vectors for cylinder radius/ sphere cap equator and cones
        // Direction is j basis (0,1,0)
        basisVectors: any;

        constructor() {

            //initialize basisVectors
            let nvecs = [];

            let subdivisions = 4; // including the initial 2, eg. 4 => 16 subintervals
            let N = Math.pow(2, subdivisions);  // eg. 2**4 = 16 subintervals in total
            let i = 2;  // start with 2 subdivisions already done
            let M = Math.pow(2, i); // 4
            let spacing = N / M;  // 16/4 = 4; if there were 5 subdivs, then 32/4 = 8.
            let j: number;

            nvecs[0] = new Vector3(-1, 0, 0);
            nvecs[spacing] = new Vector3(0, 0, 1);
            nvecs[spacing * 2] = new Vector3(1, 0, 0);
            nvecs[spacing * 3] = new Vector3(0, 0, -1);

            for (i = 3; i <= subdivisions; i++) {
                // eg. i=3, we need to add 2**(3-1) = 4 new vecs. Call it M.
                // their spacing is N/M, eg. N=16, M=4, N/M=4; M=8, N/M=2.
                // they start off at half this spacing
                // and are equal to the average of the two vectors on either side
                M = Math.pow(2, (i - 1));
                spacing = N / M;
                for (j = 0; j < (M - 1); j++) {
                    nvecs[spacing / 2 + j * spacing] = nvecs[j * spacing].clone().add(nvecs[(j + 1) * spacing]).normalize();
                }
                // treat the last one specially so it wraps around to zero
                j = M - 1;
                nvecs[spacing / 2 + j * spacing] = nvecs[j * spacing].clone().add(nvecs[0]).normalize();
            }

            this.basisVectors = nvecs;
        };

        getVerticesForRadius(radius: any, cap: CAP, capType: any) {
            if (typeof (this.cache) !== "undefined" && this.cache[radius] !== undefined)
                if (this.cache[radius][cap + capType] !== undefined)
                    return this.cache[radius][cap + capType];

            var w = this.basisVectors.length;
            var nvecs = [], norms = [];
            var n;

            for (var i = 0; i < w; i++) {
                // bottom
                nvecs.push(this.basisVectors[i].clone().multiplyScalar(radius));
                // top
                nvecs.push(this.basisVectors[i].clone().multiplyScalar(radius));

                // NOTE: this normal is used for constructing sphere caps -
                // cylinder normals taken care of in drawCylinder
                n = this.basisVectors[i].clone().normalize();
                norms.push(n);
                norms.push(n);
            }

            // norms[0]

            var verticesRows = [];

            // Require that heightSegments is even and >= 2
            // Equator points at h/2 (theta = pi/2)
            // (repeated) polar points at 0 and h (theta = 0 and pi)
            var heightSegments = 10, widthSegments = w; // 16 or however many
            // basis vectors for
            // cylinder

            if (heightSegments % 2 !== 0 || !heightSegments) {
                console.error("heightSegments must be even");
                return null;
            }

            var phiStart = 0;
            var phiLength = Math.PI * 2;

            var thetaStart = 0;
            var thetaLength = Math.PI;

            var x: number, y:number;
            var polar = false, equator = false;

            for (y = 0; y <= heightSegments; y++) {

                polar = (y === 0 || y === heightSegments) ? true : false;
                equator = (y === heightSegments / 2) ? true : false;

                var verticesRow = [], toRow = [];

                for (x = 0; x <= widthSegments; x++) {

                    // Two vertices rows for equator pointing to previously
                    // constructed cyl points
                    if (equator) {
                        var xi = (x < widthSegments) ? 2 * x : 0;
                        toRow.push(xi + 1);
                        verticesRow.push(xi);

                        continue;
                    }

                    var u = x / widthSegments;
                    var v = y / heightSegments;

                    // Only push first polar point

                    if (!polar || x === 0) {

                        if (x < widthSegments) {
                            var vertex = new Vector3();
                            vertex.x = -radius *
                                Math.cos(phiStart + u * phiLength) *
                                Math.sin(thetaStart + v * thetaLength);
                            if (cap == 1)
                                vertex.y = 0;
                            else
                                vertex.y = radius * Math.cos(thetaStart + v * thetaLength);

                            vertex.z = radius *
                                Math.sin(phiStart + u * phiLength) *
                                Math.sin(thetaStart + v * thetaLength);

                            if (Math.abs(vertex.x) < 1e-5)
                                vertex.x = 0;
                            if (Math.abs(vertex.y) < 1e-5)
                                vertex.y = 0;
                            if (Math.abs(vertex.z) < 1e-5)
                                vertex.z = 0;

                            if (cap == CAP.FLAT) {
                                n = new Vector3(0, Math.cos(thetaStart + v * thetaLength), 0);
                                n.normalize();
                            }
                            else {
                                n = new Vector3(vertex.x, vertex.y, vertex.z);
                                n.normalize();
                            }

                            nvecs.push(vertex);
                            norms.push(n);

                            verticesRow.push(nvecs.length - 1);
                        }

                        // last point is just the first point for this row
                        else {
                            verticesRow.push(nvecs.length - widthSegments);
                        }

                    }

                    // x > 0; index to already added point
                    else if (polar)
                        verticesRow.push(nvecs.length - 1);

                }

                // extra equator row
                if (equator)
                    verticesRows.push(toRow);

                verticesRows.push(verticesRow);

            }

            var obj = {
                vertices: nvecs,
                normals: norms,
                verticesRows: verticesRows,
                w: widthSegments,
                h: heightSegments
            };

            if (!(radius in this.cache)) this.cache[radius] = {};
            this.cache[radius][cap + capType] = obj;

            return obj;

        }
    };

    var cylVertexCache = new CylVertexCache();

    /** 
     * Create a cylinder 
     * @memberof GLDraw 
     * @param {Geometry}
     *            geo
     * @param {Point}
     *            from
     * @param {Point}
     *            to
     * @param {number}
     *            radius
     * @param {Color}
     *            color
     * @param {CAP} fromCap - 0 for none, 1 for flat, 2 for round
     * @param {CAP} toCap = 0 for none, 1 for flat, 2 for round
     *            
     * */
    export function drawCylinder(geo: Geometry, from: any, to: any, radius: number, color: Color | Color[], fromCap:CAP|string = 0, toCap:CAP|string = 0) {
        if (!from || !to)
            return;

        let getcap = function(c: CAP|string): CAP {
            if(typeof c === "string") {
                let s = <string>c;
                if(s.toLowerCase() == 'flat') {
                    return CAP.FLAT;
                } else if(s.toLowerCase() == 'round') {
                    return CAP.ROUND;
                } else {
                    return CAP.NONE;
                }
            } else {
                return <CAP>c;
            }
        }
        fromCap = getcap(fromCap);
        toCap = getcap(toCap);
        
        // vertices
        var drawcaps = toCap || fromCap;
        color = color || ({ r: 0, g: 0, b: 0 } as Color);

        var e = getRotationMatrix(to.x-from.x, to.y-from.y, to.z-from.z);
        // get orthonormal vectors from cache
        // TODO: Will have orient with model view matrix according to direction

        var vobj = cylVertexCache.getVerticesForRadius(radius, toCap, "to");
        // w (n) corresponds to the number of orthonormal vectors for cylinder
        // (default 16)
        var n = vobj.w, h = vobj.h;

        // get orthonormal vector
        var n_verts = (drawcaps) ? h * n + 2 : 2 * n;

        var geoGroup = geo.updateGeoGroup(n_verts);

        var vertices = vobj.vertices, normals = vobj.normals, verticesRows = vobj.verticesRows;
        var toRow = verticesRows[h / 2], fromRow = verticesRows[h / 2 + 1];

        var start = geoGroup.vertices;
        var offset, faceoffset;
        var i, x, y, z;

        var vertexArray = geoGroup.vertexArray;
        var normalArray = geoGroup.normalArray;
        var colorArray = geoGroup.colorArray;
        var faceArray = geoGroup.faceArray;
        // add vertices, opposing vertices paired together
        for (i = 0; i < n; ++i) {

            var vi = 2 * i;

            x = e[0] * vertices[vi].x + e[3] * vertices[vi].y + e[6] * vertices[vi].z;
            y = e[1] * vertices[vi].x + e[4] * vertices[vi].y + e[7] * vertices[vi].z;
            z = e[5] * vertices[vi].y + e[8] * vertices[vi].z;

            // var xn = x/radius, yn = y/radius, zn = z/radius;

            offset = 3 * (start + vi);
            faceoffset = geoGroup.faceidx;

            // from
            vertexArray[offset] = x + from.x;
            vertexArray[offset + 1] = y + from.y;
            vertexArray[offset + 2] = z + from.z;
            // to
            vertexArray[offset + 3] = x + to.x;
            vertexArray[offset + 4] = y + to.y;
            vertexArray[offset + 5] = z + to.z;

            // normals
            normalArray[offset] = x;
            normalArray[offset + 3] = x;
            normalArray[offset + 1] = y;
            normalArray[offset + 4] = y;
            normalArray[offset + 2] = z;
            normalArray[offset + 5] = z;

            // colors
            colorArray[offset] = (color as Color).r;
            colorArray[offset + 3] = (color as Color).r;
            colorArray[offset + 1] = (color as Color).g;
            colorArray[offset + 4] = (color as Color).g;
            colorArray[offset + 2] = (color as Color).b;
            colorArray[offset + 5] = (color as Color).b;

            // faces
            // 0 - 2 - 1
            faceArray[faceoffset] = fromRow[i] + start;
            faceArray[faceoffset + 1] = fromRow[i + 1] + start;
            faceArray[faceoffset + 2] = toRow[i] + start;
            // 1 - 2 - 3
            faceArray[faceoffset + 3] = toRow[i] + start;
            faceArray[faceoffset + 4] = fromRow[i + 1] + start;
            faceArray[faceoffset + 5] = toRow[i + 1] + start;

            geoGroup.faceidx += 6;

        }

        // SPHERE CAPS
        if (drawcaps) {
            // h - sphere rows, verticesRows.length - 2

            var ystart = (toCap) ? 0 : h / 2;
            var yend = (fromCap) ? h + 1 : h / 2 + 1;
            var v1, v2, v3, v4, x1, x2, x3, x4, y1, y2, y3, y4, z1, z2, z3, z4, nx1, nx2, nx3, nx4, ny1, ny2, ny3, ny4, nz1, nz2, nz3, nz4, v1offset, v2offset, v3offset, v4offset;

            for (y = ystart; y < yend; y++) {
                if (y === h / 2)
                    continue;
                // n number of points for each level (verticesRows[i].length -
                // 1)
                var cap = (y <= h / 2) ? to : from;
                var toObj = cylVertexCache.getVerticesForRadius(radius, toCap, "to");
                var fromObj = cylVertexCache.getVerticesForRadius(radius, fromCap, "from");
                if (cap === to) {
                    vertices = toObj.vertices;
                    normals = toObj.normals;
                    verticesRows = toObj.verticesRows;
                } else if (cap == from) {
                    vertices = fromObj.vertices;
                    normals = fromObj.normals;
                    verticesRows = fromObj.verticesRows;
                }
                for (x = 0; x < n; x++) {

                    faceoffset = geoGroup.faceidx;

                    v1 = verticesRows[y][x + 1];
                    v1offset = (v1 + start) * 3;
                    v2 = verticesRows[y][x];
                    v2offset = (v2 + start) * 3;
                    v3 = verticesRows[y + 1][x];
                    v3offset = (v3 + start) * 3;
                    v4 = verticesRows[y + 1][x + 1];
                    v4offset = (v4 + start) * 3;

                    // rotate sphere vectors
                    x1 = e[0] * vertices[v1].x + e[3] * vertices[v1].y + e[6] * vertices[v1].z;
                    x2 = e[0] * vertices[v2].x + e[3] * vertices[v2].y + e[6] * vertices[v2].z;
                    x3 = e[0] * vertices[v3].x + e[3] * vertices[v3].y + e[6] * vertices[v3].z;
                    x4 = e[0] * vertices[v4].x + e[3] * vertices[v4].y + e[6] * vertices[v4].z;

                    y1 = e[1] * vertices[v1].x + e[4] * vertices[v1].y + e[7] * vertices[v1].z;
                    y2 = e[1] * vertices[v2].x + e[4] * vertices[v2].y + e[7] * vertices[v2].z;
                    y3 = e[1] * vertices[v3].x + e[4] * vertices[v3].y + e[7] * vertices[v3].z;
                    y4 = e[1] * vertices[v4].x + e[4] * vertices[v4].y + e[7] * vertices[v4].z;

                    z1 = e[5] * vertices[v1].y + e[8] * vertices[v1].z;
                    z2 = e[5] * vertices[v2].y + e[8] * vertices[v2].z;
                    z3 = e[5] * vertices[v3].y + e[8] * vertices[v3].z;
                    z4 = e[5] * vertices[v4].y + e[8] * vertices[v4].z;

                    vertexArray[v1offset] = x1 + cap.x;
                    vertexArray[v2offset] = x2 + cap.x;
                    vertexArray[v3offset] = x3 + cap.x;
                    vertexArray[v4offset] = x4 + cap.x;

                    vertexArray[v1offset + 1] = y1 + cap.y;
                    vertexArray[v2offset + 1] = y2 + cap.y;
                    vertexArray[v3offset + 1] = y3 + cap.y;
                    vertexArray[v4offset + 1] = y4 + cap.y;

                    vertexArray[v1offset + 2] = z1 + cap.z;
                    vertexArray[v2offset + 2] = z2 + cap.z;
                    vertexArray[v3offset + 2] = z3 + cap.z;
                    vertexArray[v4offset + 2] = z4 + cap.z;

                    colorArray[v1offset] = (color as Color).r;
                    colorArray[v2offset] = (color as Color).r;
                    colorArray[v3offset] = (color as Color).r;
                    colorArray[v4offset] = (color as Color).r;

                    colorArray[v1offset + 1] = (color as Color).g;
                    colorArray[v2offset + 1] = (color as Color).g;
                    colorArray[v3offset + 1] = (color as Color).g;
                    colorArray[v4offset + 1] = (color as Color).g;

                    colorArray[v1offset + 2] = (color as Color).b;
                    colorArray[v2offset + 2] = (color as Color).b;
                    colorArray[v3offset + 2] = (color as Color).b;
                    colorArray[v4offset + 2] = (color as Color).b;

                    nx1 = e[0] * normals[v1].x + e[3] * normals[v1].y + e[6] * normals[v1].z;
                    nx2 = e[0] * normals[v2].x + e[3] * normals[v2].y + e[6] * normals[v2].z;
                    nx3 = e[0] * normals[v3].x + e[3] * normals[v3].y + e[6] * normals[v3].z;
                    nx4 = e[0] * normals[v4].x + e[3] * normals[v4].y + e[6] * normals[v4].z;

                    ny1 = e[1] * normals[v1].x + e[4] * normals[v1].y + e[7] * normals[v1].z;
                    ny2 = e[1] * normals[v2].x + e[4] * normals[v2].y + e[7] * normals[v2].z;
                    ny3 = e[1] * normals[v3].x + e[4] * normals[v3].y + e[7] * normals[v3].z;
                    ny4 = e[1] * normals[v4].x + e[4] * normals[v4].y + e[7] * normals[v4].z;

                    nz1 = e[5] * normals[v1].y + e[8] * normals[v1].z;
                    nz2 = e[5] * normals[v2].y + e[8] * normals[v2].z;
                    nz3 = e[5] * normals[v3].y + e[8] * normals[v3].z;
                    nz4 = e[5] * normals[v4].y + e[8] * normals[v4].z;

                    // if (Math.abs(vobj.sphereVertices[v1].y) === radius) {

                    if (y === 0) {//to center circle
                        // face = [v1, v3, v4];
                        // norm = [n1, n3, n4];

                        normalArray[v1offset] = nx1;
                        normalArray[v3offset] = nx3;
                        normalArray[v4offset] = nx4;
                        normalArray[v1offset + 1] = ny1;
                        normalArray[v3offset + 1] = ny3;
                        normalArray[v4offset + 1] = ny4;
                        normalArray[v1offset + 2] = nz1;
                        normalArray[v3offset + 2] = nz3;
                        normalArray[v4offset + 2] = nz4;

                        faceArray[faceoffset] = v1 + start;
                        faceArray[faceoffset + 1] = v3 + start;
                        faceArray[faceoffset + 2] = v4 + start;

                        geoGroup.faceidx += 3;

                    }

                    // else if (Math.abs(vobj.sphereVertices[v3].y) === radius)
                    // {
                    else if (y === yend - 1) {//from end center circle
                        // face = [v1, v2, v3];
                        // norm = [n1, n2, n3];

                        normalArray[v1offset] = nx1;
                        normalArray[v2offset] = nx2;
                        normalArray[v3offset] = nx3;
                        normalArray[v1offset + 1] = ny1;
                        normalArray[v2offset + 1] = ny2;
                        normalArray[v3offset + 1] = ny3;
                        normalArray[v1offset + 2] = nz1;
                        normalArray[v2offset + 2] = nz2;
                        normalArray[v3offset + 2] = nz3;

                        faceArray[faceoffset] = v1 + start;
                        faceArray[faceoffset + 1] = v2 + start;
                        faceArray[faceoffset + 2] = v3 + start;

                        geoGroup.faceidx += 3;

                    }

                    else { // the rest of the circles
                        // face = [v1, v2, v3, v4];
                        // norm = [n1, n2, n3, n4];

                        normalArray[v1offset] = nx1;
                        normalArray[v2offset] = nx2;
                        normalArray[v4offset] = nx4;
                        normalArray[v1offset + 1] = ny1;
                        normalArray[v2offset + 1] = ny2;
                        normalArray[v4offset + 1] = ny4;
                        normalArray[v1offset + 2] = nz1;
                        normalArray[v2offset + 2] = nz2;
                        normalArray[v4offset + 2] = nz4;

                        normalArray[v2offset] = nx2;
                        normalArray[v3offset] = nx3;
                        normalArray[v4offset] = nx4;
                        normalArray[v2offset + 1] = ny2;
                        normalArray[v3offset + 1] = ny3;
                        normalArray[v4offset + 1] = ny4;
                        normalArray[v2offset + 2] = nz2;
                        normalArray[v3offset + 2] = nz3;
                        normalArray[v4offset + 2] = nz4;

                        faceArray[faceoffset] = v1 + start;
                        faceArray[faceoffset + 1] = v2 + start;
                        faceArray[faceoffset + 2] = v4 + start;

                        faceArray[faceoffset + 3] = v2 + start;
                        faceArray[faceoffset + 4] = v3 + start;
                        faceArray[faceoffset + 5] = v4 + start;

                        geoGroup.faceidx += 6;
                    }

                }
            }

        }

        geoGroup.vertices += n_verts;
    };


    /** Create a cone 
     * @memberof GLDraw
     * @param {Geometry}
     *            geo
     * @param {Point}
     *            from
     * @param {Point}
     *            to
     * @param {number}
     *            radius
     * @param {Color}
     *            color
     *            */
    export function drawCone (geo: Geometry, from: any, to: any, radius: number, color?: Color) {
        if (!from || !to)
            return;

        // TODO: check if from and to do not contain x,y,z and if  so generate a center based on the passed selections

        color = color || ({ r: 0, g: 0, b: 0 } as Color);

        let ndir = new Vector3(to.x-from.x, to.y-from.y, to.z-from.z);
        var e = getRotationMatrix(ndir.x, ndir.y, ndir.z);
        ndir = ndir.normalize();

        // n vertices around bottom plust the two points
        var n = cylVertexCache.basisVectors.length;
        var basis = cylVertexCache.basisVectors;
        var n_verts = n + 2;

        //setup geo structures
        var geoGroup = geo.updateGeoGroup(n_verts);
        var start = geoGroup.vertices;
        var offset, faceoffset;
        var i, x, y, z;
        var vertexArray = geoGroup.vertexArray;
        var normalArray = geoGroup.normalArray;
        var colorArray = geoGroup.colorArray;
        var faceArray = geoGroup.faceArray;

        offset = start * 3;
        //base point first vertex
        vertexArray[offset] = from.x;
        vertexArray[offset + 1] = from.y;
        vertexArray[offset + 2] = from.z;
        normalArray[offset] = -ndir.x;
        normalArray[offset + 1] = -ndir.y;
        normalArray[offset + 2] = -ndir.z;
        colorArray[offset] = color.r;
        colorArray[offset + 1] = color.g;
        colorArray[offset + 2] = color.b;

        //second vertex top
        vertexArray[offset + 3] = to.x;
        vertexArray[offset + 4] = to.y;
        vertexArray[offset + 5] = to.z;

        normalArray[offset + 3] = ndir.x;
        normalArray[offset + 4] = ndir.y;
        normalArray[offset + 5] = ndir.z;
        colorArray[offset + 3] = color.r;
        colorArray[offset + 4] = color.g;
        colorArray[offset + 5] = color.b;

        offset += 6;

        // add circle vertices
        for (i = 0; i < n; ++i) {
            var vec = basis[i].clone();
            vec.multiplyScalar(radius);
            x = e[0] * vec.x + e[3] * vec.y + e[6] * vec.z;
            y = e[1] * vec.x + e[4] * vec.y + e[7] * vec.z;
            z = e[5] * vec.y + e[8] * vec.z;

            // from
            vertexArray[offset] = x + from.x;
            vertexArray[offset + 1] = y + from.y;
            vertexArray[offset + 2] = z + from.z;

            // normals
            normalArray[offset] = x;
            normalArray[offset + 1] = y;
            normalArray[offset + 2] = z;

            // colors
            colorArray[offset] = color.r;
            colorArray[offset + 1] = color.g;
            colorArray[offset + 2] = color.b;

            offset += 3;

        }
        geoGroup.vertices += (n + 2);
        //faces
        faceoffset = geoGroup.faceidx;
        for (i = 0; i < n; i++) {
            //two neighboring circle vertices
            var v1 = start + 2 + i;
            var v2 = start + 2 + ((i + 1) % n);

            faceArray[faceoffset] = v1;
            faceArray[faceoffset + 1] = v2;
            faceArray[faceoffset + 2] = start;
            faceoffset += 3;
            faceArray[faceoffset] = v1;
            faceArray[faceoffset + 1] = v2;
            faceArray[faceoffset + 2] = start + 1;
            faceoffset += 3;
        }
        geoGroup.faceidx += 6 * n;
    };

    interface MyObject {
        vertices: any[];
        verticesRows: any[][];
        normals: any[];
     }

    // Sphere component sphereVertexCache
    class  SphereVertexCache {
        private cache = new Map<number, Map<number, any>>(); //sphereQuality then radius
        constructor() {}

        getVerticesForRadius(radius: number, sphereQuality: any) {
            sphereQuality = sphereQuality || 2;

            if (!this.cache.has(sphereQuality))  {
                this.cache.set(sphereQuality, new Map<number,any>());
            }
            let radiusCache = this.cache.get(sphereQuality);
            if (radiusCache.has(radius))
                return radiusCache.get(radius);

            var obj: MyObject = {
                vertices: [],
                verticesRows: [],
                normals: []
            };
            // scale quality with radius heuristically
            var widthSegments = 16 * sphereQuality;
            var heightSegments = 10 * sphereQuality;
            if (radius < 1) {
                widthSegments = 10 * sphereQuality;
                heightSegments = 8 * sphereQuality;
            }

            var phiStart = 0;
            var phiLength = Math.PI * 2;

            var thetaStart = 0;
            var thetaLength = Math.PI;

            var x, y;

            for (y = 0; y <= heightSegments; y++) {

                let verticesRow = [];
                for (x = 0; x <= widthSegments; x++) {

                    let u = x / widthSegments;
                    let v = y / heightSegments;

                    let vx = -radius * Math.cos(phiStart + u * phiLength) *
                        Math.sin(thetaStart + v * thetaLength);
                    let vy = radius * Math.cos(thetaStart + v * thetaLength);
                    let vz = radius * Math.sin(phiStart + u * phiLength) *
                        Math.sin(thetaStart + v * thetaLength);

                    var n = new Vector3(vx, vy, vz);
                    n.normalize();

                    obj.vertices.push({x: vx, y: vy, z: vz});
                    obj.normals.push(n);

                    verticesRow.push(obj.vertices.length - 1);

                }

                obj.verticesRows.push(verticesRow);

            }

            radiusCache.set(radius, obj);
            return obj;
        }

    };
    var sphereVertexCache = new SphereVertexCache();

    /** Create a sphere.
     * @memberof GLDraw
     * @param {Geometry}
     *            geo
     * @param {Point}
     *            pos
     * @param {number}
     *            radius
     * @param {Color}
     *            color
     * @param {number} 
     *            sphereQuality - Quality of sphere (default 2, higher increases number of triangles)
     */
    export function drawSphere(geo:Geometry, pos: any, radius: number, color: Colored, sphereQuality?: number) {

        var vobj = sphereVertexCache.getVerticesForRadius(radius, sphereQuality);

        var vertices = vobj.vertices;
        var normals = vobj.normals;

        var geoGroup = geo.updateGeoGroup(vertices.length);

        var start = geoGroup.vertices;
        var vertexArray = geoGroup.vertexArray;
        var colorArray = geoGroup.colorArray;
        var faceArray = geoGroup.faceArray;
        var lineArray = geoGroup.lineArray;
        var normalArray = geoGroup.normalArray;

        for (let i = 0, il = vertices.length; i < il; ++i) {
            let offset = 3 * (start + i);
            let v = vertices[i];

            vertexArray[offset] = (v.x + pos.x);
            vertexArray[offset + 1] = (v.y + pos.y);
            vertexArray[offset + 2] = (v.z + pos.z);

            colorArray[offset] = (color as Colored).r;
            colorArray[offset + 1] = (color as Colored).g;
            colorArray[offset + 2] = (color as Colored).b;

        }

        geoGroup.vertices += vertices.length;

        let verticesRows = vobj.verticesRows;
        let h = verticesRows.length - 1;

        for (let y = 0; y < h; y++) {
            let w = verticesRows[y].length - 1;
            for (let x = 0; x < w; x++) {

                let faceoffset = geoGroup.faceidx, lineoffset = geoGroup.lineidx;

                let v1 = verticesRows[y][x + 1] + start, v1offset = v1 * 3;
                let v2 = verticesRows[y][x] + start, v2offset = v2 * 3;
                let v3 = verticesRows[y + 1][x] + start, v3offset = v3 * 3;
                let v4 = verticesRows[y + 1][x + 1] + start, v4offset = v4 * 3;

                let n1 = normals[v1 - start];
                let n2 = normals[v2 - start];
                let n3 = normals[v3 - start];
                let n4 = normals[v4 - start];

                if (Math.abs(vertices[v1 - start].y) === radius) {
                    // face = [v1, v3, v4];
                    // norm = [n1, n3, n4];

                    normalArray[v1offset] = n1.x;
                    normalArray[v3offset] = n3.x;
                    normalArray[v4offset] = n4.x;
                    normalArray[v1offset + 1] = n1.y;
                    normalArray[v3offset + 1] = n3.y;
                    normalArray[v4offset + 1] = n4.y;
                    normalArray[v1offset + 2] = n1.z;
                    normalArray[v3offset + 2] = n3.z;
                    normalArray[v4offset + 2] = n4.z;

                    faceArray[faceoffset] = v1;
                    faceArray[faceoffset + 1] = v3;
                    faceArray[faceoffset + 2] = v4;

                    lineArray[lineoffset] = v1;
                    lineArray[lineoffset + 1] = v3;
                    lineArray[lineoffset + 2] = v1;
                    lineArray[lineoffset + 3] = v4;
                    lineArray[lineoffset + 4] = v3;
                    lineArray[lineoffset + 5] = v4;

                    geoGroup.faceidx += 3;
                    geoGroup.lineidx += 6;

                } else if (Math.abs(vertices[v3 - start].y) === radius) {
                    // face = [v1, v2, v3];
                    // norm = [n1, n2, n3];

                    normalArray[v1offset] = n1.x;
                    normalArray[v2offset] = n2.x;
                    normalArray[v3offset] = n3.x;
                    normalArray[v1offset + 1] = n1.y;
                    normalArray[v2offset + 1] = n2.y;
                    normalArray[v3offset + 1] = n3.y;
                    normalArray[v1offset + 2] = n1.z;
                    normalArray[v2offset + 2] = n2.z;
                    normalArray[v3offset + 2] = n3.z;

                    faceArray[faceoffset] = v1;
                    faceArray[faceoffset + 1] = v2;
                    faceArray[faceoffset + 2] = v3;

                    lineArray[lineoffset] = v1;
                    lineArray[lineoffset + 1] = v2;
                    lineArray[lineoffset + 2] = v1;
                    lineArray[lineoffset + 3] = v3;
                    lineArray[lineoffset + 4] = v2;
                    lineArray[lineoffset + 5] = v3;

                    geoGroup.faceidx += 3;
                    geoGroup.lineidx += 6;

                } else {
                    // face = [v1, v2, v3, v4];
                    // norm = [n1, n2, n3, n4];

                    normalArray[v1offset] = n1.x;
                    normalArray[v2offset] = n2.x;
                    normalArray[v4offset] = n4.x;
                    normalArray[v1offset + 1] = n1.y;
                    normalArray[v2offset + 1] = n2.y;
                    normalArray[v4offset + 1] = n4.y;
                    normalArray[v1offset + 2] = n1.z;
                    normalArray[v2offset + 2] = n2.z;
                    normalArray[v4offset + 2] = n4.z;

                    normalArray[v2offset] = n2.x;
                    normalArray[v3offset] = n3.x;
                    normalArray[v4offset] = n4.x;
                    normalArray[v2offset + 1] = n2.y;
                    normalArray[v3offset + 1] = n3.y;
                    normalArray[v4offset + 1] = n4.y;
                    normalArray[v2offset + 2] = n2.z;
                    normalArray[v3offset + 2] = n3.z;
                    normalArray[v4offset + 2] = n4.z;

                    faceArray[faceoffset] = v1;
                    faceArray[faceoffset + 1] = v2;
                    faceArray[faceoffset + 2] = v4;

                    faceArray[faceoffset + 3] = v2;
                    faceArray[faceoffset + 4] = v3;
                    faceArray[faceoffset + 5] = v4;

                    lineArray[lineoffset] = v1;
                    lineArray[lineoffset + 1] = v2;
                    lineArray[lineoffset + 2] = v1;
                    lineArray[lineoffset + 3] = v4;

                    lineArray[lineoffset + 4] = v2;
                    lineArray[lineoffset + 5] = v3;
                    lineArray[lineoffset + 6] = v3;
                    lineArray[lineoffset + 7] = v4;

                    geoGroup.faceidx += 6;
                    geoGroup.lineidx += 8;

                }

            }
        }

    };

}