GLShape.ts

import { Geometry, Material } from "./WebGL";
import { Sphere, Cylinder, Triangle } from "./WebGL/shapes";
import { Vector3, XYZ } from "./WebGL/math";
import { clamp } from "./WebGL/math";
import { DoubleSide } from "./WebGL";
import { Color, CC, ColorSpec, Colored } from "./colors";
import { MarchingCube } from "./ProteinSurface4";
import { VolumeData } from "./VolumeData";
import { MeshDoubleLambertMaterial, MeshLambertMaterial, Object3D, Coloring, Mesh, LineBasicMaterial, Line, LineStyle } from "./WebGL";
import { CAP, GLDraw } from "./GLDraw"
import { subdivide_spline } from "./glcartoon";
import { adjustVolumeStyle, extend, Func, makeFunction } from "./utilities";
import { GradientType } from "./Gradient";


/**
 * A GLShape is a collection of user specified shapes.
 * 
 * @class
 * @extends {ShapeSpec}
 * @param {number} sid - Unique identifier
 * @param {ShapeSpec} stylespec - shape style specification
 */
export class GLShape {

    // Marching cube, to match with protein surface generation
    private static ISDONE = 2;

    private static finalizeGeo(geo) {
        //to avoid creating a bunch of geometries, we leave geoGroup untruncated
        //until render is called, at which point we truncate; 
        //successive called up updateGeo will return a new geometry
        var geoGroup = geo.updateGeoGroup(0);
        if (geoGroup.vertices > 0) {
            geoGroup.truncateArrayBuffers(true, true);
        }
    };

    /* 
     * 
     * @param {Geometry}
     *            geo
     * @param {Color | colorlike} color
     */
    static updateColor(geo: Geometry, color) {

        color = color || CC.color(color);
        geo.colorsNeedUpdate = true;

        var r, g, b;
        if (color.constructor !== Array) {
            r = color.r;
            g = color.g;
            b = color.b;
        }


        for (let gg in geo.geometryGroups) {

            let geoGroup = geo.geometryGroups[gg];
            let colorArr = geoGroup.colorArray;

            for (let i = 0, il = geoGroup.vertices; i < il; ++i) {

                if (color.constructor === Array) {
                    let c = color[i];
                    r = c.r;
                    g = c.g;
                    b = c.b;
                }

                colorArr[i * 3] = r;
                colorArr[i * 3 + 1] = g;
                colorArr[i * 3 + 2] = b;
            }
        }
    };


    /*
     * @param {GLShape}
     *            shape
     * @param {geometryGroup}
     *            geoGroup
     * @param {ArrowSpec}
     *            spec
     */
    static drawArrow(shape: GLShape, geo: Geometry, spec: ArrowSpec) {

        var from = spec.start, end = spec.end, radius = spec.radius,
            radiusRatio = spec.radiusRatio, mid = spec.mid, midoffset = spec.midpos;

        if (!(from && end))
            return;

        var geoGroup = geo.updateGeoGroup(51);

        // vertices

        var dir = new Vector3(end.x, end.y, end.z).sub(from);
        if (midoffset) { //absolute offset, convert to relative
            let length = dir.length();
            if (midoffset > 0) mid = midoffset / length;
            else mid = (length + midoffset) / length;
        }

        dir.multiplyScalar(mid);

        var to = new Vector3(from.x, from.y, from.z).add(dir);
        var negDir = dir.clone().negate();

        let fromv = new Vector3(from.x, from.y, from.z);
        shape.intersectionShape.cylinder.push(new Cylinder(fromv, to.clone(), radius));
        shape.intersectionShape.sphere.push(new Sphere(fromv, radius));

        // get orthonormal vector
        var nvecs = [];
        nvecs[0] = dir.clone();
        if (Math.abs(nvecs[0].x) > 0.0001)
            nvecs[0].y += 1;
        else
            nvecs[0].x += 1;
        nvecs[0].cross(dir);
        nvecs[0].normalize();

        // another orth vector
        nvecs[4] = nvecs[0].clone();
        nvecs[4].crossVectors(nvecs[0], dir);
        nvecs[4].normalize();
        nvecs[8] = nvecs[0].clone().negate();
        nvecs[12] = nvecs[4].clone().negate();

        // now quarter positions
        nvecs[2] = nvecs[0].clone().add(nvecs[4]).normalize();
        nvecs[6] = nvecs[4].clone().add(nvecs[8]).normalize();
        nvecs[10] = nvecs[8].clone().add(nvecs[12]).normalize();
        nvecs[14] = nvecs[12].clone().add(nvecs[0]).normalize();

        // eights
        nvecs[1] = nvecs[0].clone().add(nvecs[2]).normalize();
        nvecs[3] = nvecs[2].clone().add(nvecs[4]).normalize();
        nvecs[5] = nvecs[4].clone().add(nvecs[6]).normalize();
        nvecs[7] = nvecs[6].clone().add(nvecs[8]).normalize();
        nvecs[9] = nvecs[8].clone().add(nvecs[10]).normalize();
        nvecs[11] = nvecs[10].clone().add(nvecs[12]).normalize();
        nvecs[13] = nvecs[12].clone().add(nvecs[14]).normalize();
        nvecs[15] = nvecs[14].clone().add(nvecs[0]).normalize();

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

        var offset, i, n;
        // add vertices, opposing vertices paired together
        for (i = 0, n = nvecs.length; i < n; ++i) {
            offset = 3 * (start + 3 * i);
            var bottom = nvecs[i].clone().multiplyScalar(radius).add(from);
            var top = nvecs[i].clone().multiplyScalar(radius).add(to);
            var conebase = nvecs[i].clone()
                .multiplyScalar(radius * radiusRatio).add(to);

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

            vertexArray[offset + 3] = top.x;
            vertexArray[offset + 4] = top.y;
            vertexArray[offset + 5] = top.z;

            vertexArray[offset + 6] = conebase.x;
            vertexArray[offset + 7] = conebase.y;
            vertexArray[offset + 8] = conebase.z;

            if (i > 0) {
                var prev_x = vertexArray[offset - 3];
                var prev_y = vertexArray[offset - 2];
                var prev_z = vertexArray[offset - 1];

                var c = new Vector3(prev_x, prev_y, prev_z);
                var b = new Vector3(end.x, end.y, end.z), b2 = to.clone();
                var a = new Vector3(conebase.x, conebase.y, conebase.z);

                shape.intersectionShape.triangle.push(new Triangle(a, b, c));
                shape.intersectionShape.triangle.push(new Triangle(c.clone(), b2, a.clone()));
            }
        }

        geoGroup.vertices += 48;
        offset = geoGroup.vertices * 3;

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

        vertexArray[offset + 3] = to.x;
        vertexArray[offset + 4] = to.y;
        vertexArray[offset + 5] = to.z;

        vertexArray[offset + 6] = end.x;
        vertexArray[offset + 7] = end.y;
        vertexArray[offset + 8] = end.z;

        geoGroup.vertices += 3;

        // now faces
        var face, faceoffset, lineoffset;
        var t1, t2, t2b, t3, t3b, t4, t1offset, t2offset, t2boffset, t3offset, t3boffset, t4offset;
        var n1, n2, n3, n4;
        var fromi = geoGroup.vertices - 3, toi = geoGroup.vertices - 2, endi = geoGroup.vertices - 1;
        var fromoffset = fromi * 3, tooffset = toi * 3, endoffset = endi * 3;
        for (i = 0, n = nvecs.length - 1; i < n; ++i) {

            var ti = start + 3 * i;
            offset = ti * 3;
            faceoffset = geoGroup.faceidx;
            lineoffset = geoGroup.lineidx;

            t1 = ti;
            t1offset = t1 * 3;
            t2 = ti + 1;
            t2offset = t2 * 3;
            t2b = ti + 2;
            t2boffset = t2b * 3;
            t3 = ti + 4;
            t3offset = t3 * 3;
            t3b = ti + 5;
            t3boffset = t3b * 3;
            t4 = ti + 3;
            t4offset = t4 * 3;

            // face = [t1, t2, t4], [t2, t3, t4];
            // face = [t1, t2, t3, t4];

            //   norm = [nvecs[i], nvecs[i], nvecs[i + 1], nvecs[i + 1]];

            n1 = n2 = nvecs[i];
            n3 = n4 = nvecs[i + 1];

            normalArray[t1offset] = n1.x;
            normalArray[t2offset] = n2.x;
            normalArray[t4offset] = n4.x;
            normalArray[t1offset + 1] = n1.y;
            normalArray[t2offset + 1] = n2.y;
            normalArray[t4offset + 1] = n4.y;
            normalArray[t1offset + 2] = n1.z;
            normalArray[t2offset + 2] = n2.z;
            normalArray[t4offset + 2] = n4.z;

            normalArray[t2offset] = n2.x;
            normalArray[t3offset] = n3.x;
            normalArray[t4offset] = n4.x;
            normalArray[t2offset + 1] = n2.y;
            normalArray[t3offset + 1] = n3.y;
            normalArray[t4offset + 1] = n4.y;
            normalArray[t2offset + 2] = n2.z;
            normalArray[t3offset + 2] = n3.z;
            normalArray[t4offset + 2] = n4.z;

            normalArray[t2boffset] = n2.x;
            normalArray[t3boffset] = n3.x;
            normalArray[t2boffset + 1] = n2.y;
            normalArray[t3boffset + 1] = n3.y;
            normalArray[t2boffset + 2] = n2.z;
            normalArray[t3boffset + 2] = n3.z;

            // sides
            faceArray[faceoffset] = t1;
            faceArray[faceoffset + 1] = t2;
            faceArray[faceoffset + 2] = t4;
            faceArray[faceoffset + 3] = t2;
            faceArray[faceoffset + 4] = t3;
            faceArray[faceoffset + 5] = t4;
            // caps
            faceArray[faceoffset + 6] = t1;
            faceArray[faceoffset + 7] = t4;
            faceArray[faceoffset + 8] = fromi;
            faceArray[faceoffset + 9] = t2b;
            faceArray[faceoffset + 10] = toi;
            faceArray[faceoffset + 11] = t3b;
            // arrowhead
            faceArray[faceoffset + 12] = t2b;
            faceArray[faceoffset + 13] = endi;
            faceArray[faceoffset + 14] = t3b;

            // sides
            lineArray[lineoffset] = t1;
            lineArray[lineoffset + 1] = t2;
            lineArray[lineoffset + 2] = t1;
            lineArray[lineoffset + 3] = t4;
            // lineArray[lineoffset+4] = t2, lineArray[lineoffset+5] = t3;
            lineArray[lineoffset + 4] = t3;
            lineArray[lineoffset + 5] = t4;
            // caps
            lineArray[lineoffset + 6] = t1;
            lineArray[lineoffset + 7] = t4;
            // lineArray[lineoffset+10] = t1, lineArray[lineoffset+11] = fromi;
            // lineArray[lineoffset+12] = t4, lineArray[lineoffset+13] = fromi;

            lineArray[lineoffset + 8] = t2b;
            lineArray[lineoffset + 9] = t2; // toi
            lineArray[lineoffset + 10] = t2b;
            lineArray[lineoffset + 11] = t3b;
            lineArray[lineoffset + 12] = t3;
            lineArray[lineoffset + 13] = t3b; // toi
            // arrowhead
            lineArray[lineoffset + 14] = t2b;
            lineArray[lineoffset + 15] = endi;
            lineArray[lineoffset + 16] = t2b;
            lineArray[lineoffset + 17] = t3b;
            lineArray[lineoffset + 18] = endi;
            lineArray[lineoffset + 19] = t3b;

            geoGroup.faceidx += 15;
            geoGroup.lineidx += 20;

        }
        // final face

        face = [start + 45, start + 46, start + 1, start, start + 47,
        start + 2];
        //norm = [nvecs[15], nvecs[15], nvecs[0], nvecs[0]];

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

        t1 = face[0];
        t1offset = t1 * 3;
        t2 = face[1];
        t2offset = t2 * 3;
        t2b = face[4];
        t2boffset = t2b * 3;
        t3 = face[2];
        t3offset = t3 * 3;
        t3b = face[5];
        t3boffset = t3b * 3;
        t4 = face[3];
        t4offset = t4 * 3;

        n1 = n2 = nvecs[15];
        n3 = n4 = nvecs[0];

        normalArray[t1offset] = n1.x;
        normalArray[t2offset] = n2.x;
        normalArray[t4offset] = n4.x;
        normalArray[t1offset + 1] = n1.y;
        normalArray[t2offset + 1] = n2.y;
        normalArray[t4offset + 1] = n4.y;
        normalArray[t1offset + 2] = n1.z;
        normalArray[t2offset + 2] = n2.z;
        normalArray[t4offset + 2] = n4.z;

        normalArray[t2offset] = n2.x;
        normalArray[t3offset] = n3.x;
        normalArray[t4offset] = n4.x;
        normalArray[t2offset + 1] = n2.y;
        normalArray[t3offset + 1] = n3.y;
        normalArray[t4offset + 1] = n4.y;
        normalArray[t2offset + 2] = n2.z;
        normalArray[t3offset + 2] = n3.z;
        normalArray[t4offset + 2] = n4.z;

        normalArray[t2boffset] = n2.x;
        normalArray[t3boffset] = n3.x;
        normalArray[t2boffset + 1] = n2.y;
        normalArray[t3boffset + 1] = n3.y;
        normalArray[t2boffset + 2] = n2.z;
        normalArray[t3boffset + 2] = n3.z;

        // Cap normals
        dir.normalize();
        negDir.normalize();
        normalArray[fromoffset] = negDir.x;
        normalArray[tooffset] = normalArray[endoffset] = dir.x;
        normalArray[fromoffset + 1] = negDir.y;
        normalArray[tooffset + 1] = normalArray[endoffset + 1] = dir.y;
        normalArray[fromoffset + 2] = negDir.z;
        normalArray[tooffset + 2] = normalArray[endoffset + 2] = dir.z;

        // Final side
        faceArray[faceoffset] = t1;
        faceArray[faceoffset + 1] = t2;
        faceArray[faceoffset + 2] = t4;
        faceArray[faceoffset + 3] = t2;
        faceArray[faceoffset + 4] = t3;
        faceArray[faceoffset + 5] = t4;
        // final caps
        faceArray[faceoffset + 6] = t1;
        faceArray[faceoffset + 7] = t4;
        faceArray[faceoffset + 8] = fromi;
        faceArray[faceoffset + 9] = t2b;
        faceArray[faceoffset + 10] = toi;
        faceArray[faceoffset + 11] = t3b;
        // final arrowhead
        faceArray[faceoffset + 12] = t2b;
        faceArray[faceoffset + 13] = endi;
        faceArray[faceoffset + 14] = t3b;

        // sides
        lineArray[lineoffset] = t1;
        lineArray[lineoffset + 1] = t2;
        lineArray[lineoffset + 2] = t1;
        lineArray[lineoffset + 3] = t4;
        // lineArray[lineoffset+4] = t2, lineArray[lineoffset+5] = t3;
        lineArray[lineoffset + 4] = t3;
        lineArray[lineoffset + 5] = t4;
        // caps
        lineArray[lineoffset + 6] = t1;
        lineArray[lineoffset + 7] = t4;
        // lineArray[lineoffset+10] = t1, lineArray[lineoffset+11] = fromi;
        // lineArray[lineoffset+12] = t4, lineArray[lineoffset+13] = fromi;

        lineArray[lineoffset + 8] = t2b;
        lineArray[lineoffset + 9] = t2; // toi
        lineArray[lineoffset + 10] = t2b;
        lineArray[lineoffset + 11] = t3b;
        lineArray[lineoffset + 12] = t3;
        lineArray[lineoffset + 13] = t3b; // toi
        // arrowhead
        lineArray[lineoffset + 14] = t2b;
        lineArray[lineoffset + 15] = endi;
        lineArray[lineoffset + 16] = t2b;
        lineArray[lineoffset + 17] = t3b;
        lineArray[lineoffset + 18] = endi;
        lineArray[lineoffset + 19] = t3b;

        geoGroup.faceidx += 15;
        geoGroup.lineidx += 20;

    };

    // Update a bounding sphere's position and radius
    // from list of centroids and new points
    /*
     * @param {Sphere}
     *            sphere
     * @param {Object}
     *            components, centroid of all objects in shape
     * @param {Array}
     *            points, flat array of all points in shape
     * @param {int} numPoints, number of valid poitns in points
     */
    static updateBoundingFromPoints(sphere: Sphere, components, points, numPoints: number) {

        sphere.center.set(0, 0, 0);

        //previously I weighted each component's center equally, but I think
        //it is better to use all points
        let xmin = Infinity, ymin = Infinity, zmin = Infinity;
        let xmax = -Infinity, ymax = -Infinity, zmax = -Infinity;
        if (sphere.box) {
            xmin = sphere.box.min.x;
            xmax = sphere.box.max.x;
            ymin = sphere.box.min.y;
            ymax = sphere.box.max.y;
            zmin = sphere.box.min.z;
            zmax = sphere.box.max.z;
        }

        for (let i = 0, il = numPoints; i < il; i++) {
            var x = points[i * 3], y = points[i * 3 + 1], z = points[i * 3 + 2];
            if (x < xmin) xmin = x;
            if (y < ymin) ymin = y;
            if (z < zmin) zmin = z;
            if (x > xmax) xmax = x;
            if (y > ymax) ymax = y;
            if (z > zmax) zmax = z;
        }

        sphere.center.set((xmax + xmin) / 2, (ymax + ymin) / 2, (zmax + zmin) / 2);
        sphere.radius = sphere.center.distanceTo({ x: xmax, y: ymax, z: zmax });
        sphere.box = { min: { x: xmin, y: ymin, z: zmin }, max: { x: xmax, y: ymax, z: zmax } };
    };

    //helper function for adding an appropriately sized mesh
    private static addCustomGeo(shape: GLShape, geo: Geometry, mesh, color, clickable) {
        var geoGroup = geo.addGeoGroup();
        var vertexArr = mesh.vertexArr, normalArr = mesh.normalArr,
            faceArr = mesh.faceArr;

        geoGroup.vertices = vertexArr.length;
        geoGroup.faceidx = faceArr.length;

        var offset, v, a, b, c, i, il, r, g;
        var vertexArray = geoGroup.vertexArray;
        var colorArray = geoGroup.colorArray;

        if (color.constructor !== Array) {
            r = color.r;
            g = color.g;
            b = color.b;
        }
        for (i = 0, il = geoGroup.vertices; i < il; ++i) {
            offset = i * 3;
            v = vertexArr[i];
            vertexArray[offset] = v.x;
            vertexArray[offset + 1] = v.y;
            vertexArray[offset + 2] = v.z;

            if (color.constructor === Array) {
                c = color[i];
                r = c.r;
                g = c.g;
                b = c.b;
            }

            colorArray[offset] = r;
            colorArray[offset + 1] = g;
            colorArray[offset + 2] = b;
        }

        if (clickable) {
            for (i = 0, il = geoGroup.faceidx / 3; i < il; ++i) {
                offset = i * 3;
                a = faceArr[offset];
                b = faceArr[offset + 1];
                c = faceArr[offset + 2];
                var vA = new Vector3(), vB = new Vector3(), vC = new Vector3();
                shape.intersectionShape.triangle.push(new Triangle(vA.copy(vertexArr[a]),
                    vB.copy(vertexArr[b]), vC.copy(vertexArr[c])));
            }
        }

        if (clickable) {

            var center = new Vector3(0, 0, 0);
            var cnt = 0;
            for (let g = 0; g < geo.geometryGroups.length; g++) {
                center.add(geo.geometryGroups[g].getCentroid());
                cnt++;
            }
            center.divideScalar(cnt);


            GLShape.updateBoundingFromPoints(shape.boundingSphere, { centroid: center }, vertexArray, geoGroup.vertices);
        }

        geoGroup.faceArray = new Uint16Array(faceArr);

        geoGroup.truncateArrayBuffers(true, true);

        if (normalArr.length < geoGroup.vertices)
            geoGroup.setNormals();
        else {

            var normalArray = geoGroup.normalArray = new Float32Array(geoGroup.vertices * 3);
            var n;
            for (i = 0, il = geoGroup.vertices; i < il; ++i) {
                offset = i * 3;
                n = normalArr[i];
                normalArray[offset] = n.x;
                normalArray[offset + 1] = n.y;
                normalArray[offset + 2] = n.z;
            }
        }

        geoGroup.setLineIndices();
        geoGroup.lineidx = geoGroup.lineArray.length;
    };



    // handles custom shape generation from user supplied arrays
    // May need to generate normal and/or line indices
    /*
     * @param {$3Dmol.GLShape}
     *            shape
     * @param {geometry}
     *            geo
     * @param {CustomShapeSpec}
     *            customSpec
     */
    static drawCustom = function (shape: GLShape, geo: Geometry, customSpec: CustomShapeSpec) {
        var mesh = customSpec;
        var vertexArr = mesh.vertexArr;
        var faceArr = mesh.faceArr;
        if (vertexArr.length === 0 || faceArr.length === 0) {
            console
                .warn("Error adding custom shape component: No vertices and/or face indices supplied!");
        }

        var color = customSpec.color;
        if (typeof (color) == 'undefined') {
            color = shape.color;
        }
        color = CC.color(color);

        //var firstgeo = geo.geometryGroups.length;
        var splits = splitMesh(mesh);
        for (var i = 0, n = splits.length; i < n; i++) {
            GLShape.addCustomGeo(shape, geo, splits[i], splits[i].colorArr ? splits[i].colorArr : color, customSpec.clickable);
        }
    };


    /*
     * 
     * @param {$3Dmol.GLShape}
     *            shape
     * @param {ShapeSpec}
     *            stylespec
     * @returns {undefined}
     */
    static updateFromStyle(shape: GLShape, stylespec: ShapeSpec) {
        if (typeof (stylespec.color) != 'undefined') {
            shape.color = stylespec.color || new Color();
            if (!(stylespec.color instanceof Color))
                shape.color = CC.color(stylespec.color);
        } else {
            shape.color = CC.color(0);
        }
        shape.wireframe = stylespec.wireframe ? true : false;
        //opacity is the preferred nomenclature, support alpha for backwards compat
        shape.opacity = stylespec.alpha ? clamp(stylespec.alpha, 0.0,
            1.0) : 1.0;
        if (typeof (stylespec.opacity) != 'undefined') {
            shape.opacity = clamp(stylespec.opacity, 0.0, 1.0);
        }
        shape.side = (stylespec.side !== undefined) ? stylespec.side : DoubleSide;
        shape.linewidth = typeof (stylespec.linewidth) == 'undefined' ? 1 : stylespec.linewidth;
        // Click handling
        shape.clickable = stylespec.clickable ? true : false;
        shape.callback = makeFunction(stylespec.callback);
        shape.hoverable = stylespec.hoverable ? true : false;
        shape.hover_callback = makeFunction(stylespec.hover_callback);
        shape.unhover_callback = makeFunction(stylespec.unhover_callback);

        shape.hidden = stylespec.hidden;
        shape.frame = stylespec.frame;
    };

    boundingSphere: Sphere;
    intersectionShape: any;
    color: any = 0xffffff;
    hidden = false;
    wireframe = false;
    opacity = 1;
    linewidth = 1;
    clickable = false;
    callback: Func;
    hoverable = false;
    hover_callback: Func;
    unhover_callback: Func;
    frame: any;
    side = DoubleSide;
    shapePosition: any;

    private geo: Geometry;
    private linegeo: Geometry;
    private stylespec: any;
    private components: any;
    private shapeObj: any;
    private renderedShapeObj: any;
    /**
     * Custom renderable shape
     * 
     * @constructor 
     * 
     * @param {ShapeSpec} stylespec
     */
    constructor(stylespec: ShapeSpec) {

        this.stylespec = stylespec || {};

        this.boundingSphere = new Sphere();
        /** @type {IntersectionShapes} */
        this.intersectionShape = {
            sphere: [],
            cylinder: [],
            line: [],
            triangle: []
        };

        GLShape.updateFromStyle(this, this.stylespec);

        // Keep track of shape components and their centroids
        this.components = [];
        this.shapeObj = null;
        this.renderedShapeObj = null;

        this.geo = new Geometry(true);
        this.linegeo = new Geometry(true);
    };

    /** Update shape with new style specification
     * @param {ShapeSpec} newspec
       @example 
        let sphere = viewer.addSphere({center:{x:0,y:0,z:0},radius:10.0,color:'red'});
        sphere.updateStyle({color:'yellow',opacity:0.5});
        viewer.render();
     */
    updateStyle(newspec: ShapeSpec) {

        for (var prop in newspec) {
            this.stylespec[prop] = newspec[prop];
        }

        GLShape.updateFromStyle(this, this.stylespec);

        if (newspec.voldata && newspec.volscheme) {
            adjustVolumeStyle(newspec);

            //convert volumetric data into colors
            const scheme = newspec.volscheme;
            const voldata = newspec.voldata;
            const cc = CC;
            const range = scheme.range() || [-1, 1];
            this.geo.setColors(function (x, y, z) {
                let val = voldata.getVal(x, y, z);
                let col = cc.color(scheme.valueToHex(val, range));
                return col;
            });
            delete this.color;
        }
    };

    /**
     * Creates a custom shape from supplied vertex and face arrays
     * @param {CustomShapeSpec} customSpec     
     */
    public addCustom(customSpec: CustomShapeSpec) {

        customSpec.vertexArr = customSpec.vertexArr || [];
        customSpec.faceArr = customSpec.faceArr || [];
        customSpec.normalArr = customSpec.normalArr || [];

        // will split mesh as needed
        GLShape.drawCustom(this, this.geo, customSpec);
    };

    /**
     * Creates a sphere shape
     * @param {SphereSpec} sphereSpec
     @example 
     viewer.addSphere({center:{x:0,y:0,z:0},radius:10.0,color:'red'});
     
     viewer.render();
     */
    public addSphere(sphereSpec: SphereSpec) {

        if (!sphereSpec.center) {
            sphereSpec.center = new Vector3(0, 0, 0);
        }

        sphereSpec.radius = sphereSpec.radius ? clamp(sphereSpec.radius, 0, Infinity) : 1.5;
        sphereSpec.color = CC.color(sphereSpec.color);

        this.intersectionShape.sphere.push(new Sphere(sphereSpec.center, sphereSpec.radius));
 
         GLDraw.drawSphere(this.geo, sphereSpec.center,
           sphereSpec.radius, sphereSpec.color as Colored, sphereSpec.quality);

        this.components.push({
            centroid: new Vector3(sphereSpec.center.x,
                sphereSpec.center.y, sphereSpec.center.z)
        });
        var geoGroup = this.geo.updateGeoGroup(0);

        GLShape.updateBoundingFromPoints(this.boundingSphere, this.components,
            geoGroup.vertexArray, geoGroup.vertices);
    };


    /**
     * Creates a box
     * @param {BoxSpec} boxSpec
     @example 
     var shape = viewer.addShape({color:'red'});
     shape.addBox({corner: {x:1,y:2,z:0}, dimensions: {w: 4, h: 2, d: 6}});
     shape.addBox({corner: {x:-5,y:-3,z:0},
                   dimensions: { w: {x:1,y:1,z:0},
                                 h: {x:-1,y:1,z:0},
                                 d: {x:0,y:0,z:1} }});
     viewer.zoomTo();
     viewer.rotate(30);
     viewer.render();
     */
    public addBox(boxSpec: BoxSpec) {

        var dim = boxSpec.dimensions || { w: 1, h: 1, d: 1 };

        //dimensions may be scalar or vector quantities
        var w: XYZ;
        if (typeof (dim.w) == "number") {
            w = { x: dim.w, y: 0, z: 0 };
        } else {
            w = dim.w;
        }
        var h: XYZ;
        if (typeof (dim.h) == "number") {
            h = { x: 0, y: dim.h, z: 0 };
        } else {
            h = dim.h;
        }
        var d: XYZ;
        if (typeof (dim.d) == "number") {
            d = { x: 0, y: 0, z: dim.d };
        } else {
            d = dim.d;
        }

        //can position using corner OR center
        var c = boxSpec.corner;
        if (c == undefined) {
            if (boxSpec.center !== undefined) {

                c = {
                    x: boxSpec.center.x - 0.5 * (w.x + h.x + d.x),
                    y: boxSpec.center.y - 0.5 * (w.y + h.y + d.y),
                    z: boxSpec.center.z - 0.5 * (w.z + h.z + d.z)
                };
            } else { // default to origin
                c = { x: 0, y: 0, z: 0 };
            }
        }

        //8 vertices
        var uv =
            [{ x: c.x, y: c.y, z: c.z },
            { x: c.x + w.x, y: c.y + w.y, z: c.z + w.z },
            { x: c.x + h.x, y: c.y + h.y, z: c.z + h.z },
            { x: c.x + w.x + h.x, y: c.y + w.y + h.y, z: c.z + w.z + h.z },
            { x: c.x + d.x, y: c.y + d.y, z: c.z + d.z },
            { x: c.x + w.x + d.x, y: c.y + w.y + d.y, z: c.z + w.z + d.z },
            { x: c.x + h.x + d.x, y: c.y + h.y + d.y, z: c.z + h.z + d.z },
            { x: c.x + w.x + h.x + d.x, y: c.y + w.y + h.y + d.y, z: c.z + w.z + h.z + d.z }];

        //but.. so that we can have sharp issues, we want a unique normal
        //for each face - since normals are associated with vertices, need to duplicate 

        //bottom
        // 0 1
        // 2 3
        //top
        // 4 5
        // 6 7
        var verts = [];
        var faces = [];
        //bottom
        verts.splice(verts.length, 0, uv[0], uv[1], uv[2], uv[3]);
        faces.splice(faces.length, 0, 0, 2, 1, 1, 2, 3);
        var foff = 4;
        //front
        verts.splice(verts.length, 0, uv[2], uv[3], uv[6], uv[7]);
        faces.splice(faces.length, 0, foff + 0, foff + 2, foff + 1, foff + 1, foff + 2, foff + 3);
        foff += 4;
        //back
        verts.splice(verts.length, 0, uv[4], uv[5], uv[0], uv[1]);
        faces.splice(faces.length, 0, foff + 0, foff + 2, foff + 1, foff + 1, foff + 2, foff + 3);
        foff += 4;
        //top
        verts.splice(verts.length, 0, uv[6], uv[7], uv[4], uv[5]);
        faces.splice(faces.length, 0, foff + 0, foff + 2, foff + 1, foff + 1, foff + 2, foff + 3);
        foff += 4;
        //right
        verts.splice(verts.length, 0, uv[3], uv[1], uv[7], uv[5]);
        faces.splice(faces.length, 0, foff + 0, foff + 2, foff + 1, foff + 1, foff + 2, foff + 3);
        foff += 4;
        //left
        verts.splice(verts.length, 0, uv[2], uv[6], uv[0], uv[4]); // fix: was 2 0 6 4 , was flipped! will this ruin anything?
        // and is this the reason for having double sided lambert shading? the box had a flipped face
        faces.splice(faces.length, 0, foff + 0, foff + 2, foff + 1, foff + 1, foff + 2, foff + 3);
        foff += 4;

        var spec = extend({}, boxSpec);
        spec.vertexArr = verts;
        spec.faceArr = faces;
        spec.normalArr = [];
        GLShape.drawCustom(this, this.geo, spec);

        var centroid = new Vector3();
        this.components.push({
            centroid: centroid.addVectors(uv[0], uv[7]).multiplyScalar(0.5)
        });
        var geoGroup = this.geo.updateGeoGroup(0);
        GLShape.updateBoundingFromPoints(this.boundingSphere, this.components, geoGroup.vertexArray, geoGroup.vertices);
    };

    /**
     * Creates a cylinder shape
     * @param {CylinderSpec} cylinderSpec
     @example
          viewer.addCylinder({start:{x:0.0,y:0.0,z:0.0},
                              end:{x:10.0,y:0.0,z:0.0},
                              radius:1.0,
                              fromCap:1,
                              toCap:2,
                              color:'red',
                              hoverable:true,
                              clickable:true,
                              callback:function(){ this.color.setHex(0x00FFFF00);viewer.render( );},
                              hover_callback: function(){ viewer.render( );},
                              unhover_callback: function(){ this.color.setHex(0xFF000000);viewer.render( );}
                             });
          viewer.addCylinder({start:{x:0.0,y:2.0,z:0.0},
                              end:{x:0.0,y:10.0,z:0.0},
                              radius:0.5,
                              fromCap:false,
                              toCap:true,
                              color:'teal'});
          viewer.addCylinder({start:{x:15.0,y:0.0,z:0.0},
                              end:{x:20.0,y:0.0,z:0.0},
                              radius:1.0,
                              color:'black',
                              fromCap:false,
                              toCap:false});
          viewer.render();
     */
    public addCylinder(cylinderSpec: CylinderSpec) {

        var start: Vector3;
        var end: Vector3;
        if (!cylinderSpec.start) {
            start = new Vector3(0, 0, 0);
        } else {
            start = new Vector3(cylinderSpec.start.x || 0,
                cylinderSpec.start.y || 0, cylinderSpec.start.z || 0);
        }

        if (!cylinderSpec.end) {
            end = new Vector3(0, 0, 0);
        } else {
            end = new Vector3(cylinderSpec.end.x,
                cylinderSpec.end.y || 0, cylinderSpec.end.z || 0);
            if (typeof (end.x) == 'undefined') end.x = 3; //show something even if undefined
        }
        var radius = cylinderSpec.radius || 0.1;
        var color = CC.color(cylinderSpec.color);

        this.intersectionShape.cylinder.push(new Cylinder(start, end, radius));

        GLDraw.drawCylinder(this.geo, start, end, radius, color, cylinderSpec.fromCap, cylinderSpec.toCap);

        var centroid = new Vector3();
        this.components.push({
            centroid: centroid.addVectors(start, end).multiplyScalar(0.5)
        });
        var geoGroup = this.geo.updateGeoGroup(0);
        GLShape.updateBoundingFromPoints(this.boundingSphere, this.components,
            geoGroup.vertexArray, geoGroup.vertices);

    };

    /**
     * Creates a dashed cylinder shape
     * @param {CylinderSpec} cylinderSpec
     */
    public addDashedCylinder(cylinderSpec: CylinderSpec) {
  
        cylinderSpec.dashLength = cylinderSpec.dashLength || 0.25;
        cylinderSpec.gapLength = cylinderSpec.gapLength || 0.25;

        var start: Vector3;
        if (!cylinderSpec.start) start = new Vector3(0, 0, 0);
        else {
            start = new Vector3(cylinderSpec.start.x || 0,
                cylinderSpec.start.y || 0, cylinderSpec.start.z || 0);
        }

        var end: Vector3;
        if (!cylinderSpec.end) end = new Vector3(3, 0, 0);
        else {
            end = new Vector3(cylinderSpec.end.x,
                cylinderSpec.end.y || 0, cylinderSpec.end.z || 0);
            if (typeof (end.x) == 'undefined') end.x = 3; //show something even if undefined
        }

        var radius = cylinderSpec.radius || 0.1;
        var color = CC.color(cylinderSpec.color);

        var cylinderLength = Math.sqrt(Math.pow((start.x - end.x), 2) + Math.pow((start.y - end.y), 2) + Math.pow((start.z - end.z), 2));

        var count = cylinderLength / (cylinderSpec.gapLength + cylinderSpec.dashLength);

        var new_start = new Vector3(cylinderSpec.start.x || 0,
            cylinderSpec.start.y || 0, cylinderSpec.start.z || 0);
        var new_end = new Vector3(cylinderSpec.end.x,
            cylinderSpec.end.y || 0, cylinderSpec.end.z || 0);

        var gapVector = new Vector3((end.x - start.x) / (cylinderLength / cylinderSpec.gapLength), (end.y - start.y) / (cylinderLength / cylinderSpec.gapLength), (end.z - start.z) / (cylinderLength / cylinderSpec.gapLength));
        var dashVector = new Vector3((end.x - start.x) / (cylinderLength / cylinderSpec.dashLength), (end.y - start.y) / (cylinderLength / cylinderSpec.dashLength), (end.z - start.z) / (cylinderLength / cylinderSpec.dashLength));

        for (var place = 0; place < count; place++) {
            new_end = new Vector3(new_start.x + dashVector.x, new_start.y + dashVector.y, new_start.z + dashVector.z);

            this.intersectionShape.cylinder.push(new Cylinder(new_start, new_end, radius));

            GLDraw.drawCylinder(this.geo, new_start, new_end, radius, color, cylinderSpec.fromCap, cylinderSpec.toCap);

            new_start = new Vector3(new_end.x + gapVector.x, new_end.y + gapVector.y, new_end.z + gapVector.z);

        }
        var centroid = new Vector3();
        this.components.push({
            centroid: centroid.addVectors(start, end).multiplyScalar(0.5)
        });
        var geoGroup = this.geo.updateGeoGroup(0);
        GLShape.updateBoundingFromPoints(this.boundingSphere, this.components,
            geoGroup.vertexArray, geoGroup.vertices);
    };

    /**
     * Creates a curved shape
     * @param {CurveSpec} curveSpec
     */
    public addCurve(curveSpec: CurveSpec) {

        curveSpec.points = curveSpec.points || [];
        curveSpec.smooth = curveSpec.smooth || 10;
        if (typeof (curveSpec.fromCap) == "undefined") curveSpec.fromCap = 2;
        if (typeof (curveSpec.toCap) == "undefined") curveSpec.toCap = 2;

        //subdivide into smoothed spline points
        var points = subdivide_spline(curveSpec.points, curveSpec.smooth);

        if (points.length < 3) {
            console.log("Too few points in addCurve");
            return;
        }

        var radius = curveSpec.radius || 0.1;
        var color = CC.color(curveSpec.color);
        //TODO TODO - this is very inefficient, should create our
        //own water tight model with proper normals...


        //if arrows are requested, peel off enough points to fit
        //at least 2*r of arrowness
        var start = 0;
        var end = points.length - 1;
        var segmentlen = points[0].distanceTo(points[1]);
        var npts = Math.ceil(2 * radius / segmentlen);
        if (curveSpec.toArrow) {
            end -= npts;
            let arrowspec = {
                start: points[end],
                end: points[points.length - 1],
                radius: radius,
                color: color as ColorSpec,
                mid: 0.0001
            };
            this.addArrow(arrowspec);
        }
        if (curveSpec.fromArrow) {
            start += npts;
            let arrowspec = {
                start: points[start],
                end: points[0],
                radius: radius,
                color: color as ColorSpec,
                mid: 0.0001
            };
            this.addArrow(arrowspec);
        }

        var midway = Math.ceil(points.length / 2);
        var middleSpec: any = { radius: radius, color: color, fromCap: 2, toCap: 2 };
        for (var i = start; i < end; i++) {
            middleSpec.start = points[i];
            middleSpec.end = points[i + 1];
            middleSpec.fromCap = 2;
            middleSpec.toCap = 2;
            if (i < midway) {
                middleSpec.fromCap = 2;
                middleSpec.toCap = 0;
            } else if (i > midway) {
                middleSpec.fromCap = 0;
                middleSpec.toCap = 2;
            } else {
                middleSpec.fromCap = 2;
                middleSpec.toCap = 2;
            }

            this.addCylinder(middleSpec);
        }

    };

    /**
     * Creates a line shape
     * @param {LineSpec} lineSpec
     @example
     $3Dmol.download("pdb:2ABJ",viewer,{},function(){
              viewer.addLine({dashed:true,start:{x:0,y:0,z:0},end:{x:100,y:100,z:100}});
              viewer.render(callback);
          });
    
     */
    public addLine(lineSpec: LineSpec) {

        var start: Vector3;
        var end: Vector3;
        if (!lineSpec.start) {
            start = new Vector3(0, 0, 0);
        } else {
            start = new Vector3(lineSpec.start.x || 0,
                lineSpec.start.y || 0, lineSpec.start.z || 0);
        }
        if (!lineSpec.end) {
            end = new Vector3(3, 0, 0);
        } else {
            end = new Vector3(lineSpec.end.x,
                lineSpec.end.y || 0, lineSpec.end.z || 0);
            if (typeof (end.x) == 'undefined') end.x = 3; //show something even if undefined
        }

        var geoGroup = this.geo.updateGeoGroup(2);

        //make line from start to end
        //for consistency with rest of shapes, uses vertices and lines rather
        //than a separate line geometry
        var vstart = geoGroup.vertices;
        var i = vstart * 3;
        var vertexArray = geoGroup.vertexArray;
        vertexArray[i] = start.x;
        vertexArray[i + 1] = start.y;
        vertexArray[i + 2] = start.z;
        vertexArray[i + 3] = end.x;
        vertexArray[i + 4] = end.y;
        vertexArray[i + 5] = end.z;
        geoGroup.vertices += 2;

        var lineArray = geoGroup.lineArray;
        var li = geoGroup.lineidx;
        lineArray[li] = vstart;
        lineArray[li + 1] = vstart + 1;
        geoGroup.lineidx += 2;

        var centroid = new Vector3();
        this.components.push({
            centroid: centroid.addVectors(start, end).multiplyScalar(0.5)
        });
        geoGroup = this.geo.updateGeoGroup(0);
        GLShape.updateBoundingFromPoints(this.boundingSphere, this.components,
            geoGroup.vertexArray, geoGroup.vertices);
    };


    /**
     * Creates an arrow shape
     * @param {ArrowSpec} arrowSpec
     @example
      $3Dmol.download("pdb:4DM7",viewer,{},function(){
              viewer.setBackgroundColor(0xffffffff);
              viewer.addArrow({
                  start: {x:-10.0, y:0.0, z:0.0},
                  end: {x:0.0, y:-10.0, z:0.0},
                  radius: 1.0,
                  radiusRadio:1.0,
                  mid:1.0,
                  clickable:true,
                  callback:function(){
                      this.color.setHex(0xFF0000FF);
                      viewer.render( );
                  }
              });
              viewer.render();
            });
     */
    public addArrow(arrowSpec: ArrowSpec) {

        if (!arrowSpec.start) {
            arrowSpec.start = new Vector3(0, 0, 0);
        } else {
            arrowSpec.start = new Vector3(arrowSpec.start.x || 0,
                arrowSpec.start.y || 0, arrowSpec.start.z || 0);
        }

        if (arrowSpec.dir instanceof Vector3 && typeof (arrowSpec.length) === 'number') {
            var end = arrowSpec.dir.clone().multiplyScalar(arrowSpec.length).add(
                arrowSpec.start);
            arrowSpec.end = end;
        }
        else if (!arrowSpec.end) {
            arrowSpec.end = new Vector3(3, 0, 0);
        } else {
            arrowSpec.end = new Vector3(arrowSpec.end.x,
                arrowSpec.end.y || 0, arrowSpec.end.z || 0);
            if (typeof (arrowSpec.end.x) == 'undefined') arrowSpec.end.x = 3; //show something even if undefined
        }

        arrowSpec.radius = arrowSpec.radius || 0.1;

        arrowSpec.radiusRatio = arrowSpec.radiusRatio || 1.618034;

        arrowSpec.mid = (0 < arrowSpec.mid && arrowSpec.mid < 1) ? arrowSpec.mid
            : 0.618034;

        GLShape.drawArrow(this, this.geo, arrowSpec);

        var centroid = new Vector3();
        this.components.push({
            centroid: centroid.addVectors(arrowSpec.start, arrowSpec.end)
                .multiplyScalar(0.5)
        });
        var geoGroup = this.geo.updateGeoGroup(0);
        GLShape.updateBoundingFromPoints(this.boundingSphere, this.components,
            geoGroup.vertexArray, geoGroup.vertices);
    };


    static distance_from(c1: XYZ, c2: XYZ) {
        return Math.sqrt(Math.pow((c1.x - c2.x), 2) + Math.pow((c1.y - c2.y), 2) + Math.pow((c1.z - c2.z), 2));
    };

    static inSelectedRegion(coordinate: XYZ, selectedRegion, radius: number) {

        for (var i = 0; i < selectedRegion.length; i++) {
            if (GLShape.distance_from(selectedRegion[i], coordinate) <= radius)
                return true;
        }
        return false;
    };

    /**
     * Create isosurface from voluemetric data.
     * @param {VolumeData} data - volumetric input data
     * @param {IsoSurfaceSpec} isoSpec - volumetric data shape specification
     * @example //the user can specify a selected region for the isosurface 
     $.get('../test_structs/benzene-homo.cube', function(data){
              var voldata = new $3Dmol.VolumeData(data, "cube");
              viewer.addIsosurface(voldata, {isoval: 0.01,
                                             color: "blue",
                                             alpha: 0.5,
                                             smoothness: 10});
              viewer.addIsosurface(voldata, {isoval: -0.01,
                                             color: "red",
                                             smoothness: 5,
                                             opacity:0.5,
                                             wireframe:true,
                                             clickable:true,
                                             callback:
                                             function() {
                                                 this.opacity = 0.0;
                                                 viewer.render( );
                                             }});
              viewer.setStyle({}, {stick:{}});
              viewer.zoomTo();
              viewer.render();
            });
     */
    addIsosurface(data, volSpec:IsoSurfaceSpec, callback?) {//may want to cache the arrays geneerated when selectedRegion ==true

        var isoval = (volSpec.isoval !== undefined && typeof (volSpec.isoval) === "number") ? volSpec.isoval
            : 0.0;
        var voxel = (volSpec.voxel) ? true : false;
        var smoothness = (volSpec.smoothness === undefined) ? 1 : volSpec.smoothness;

        var nX = data.size.x;
        var nY = data.size.y;
        var nZ = data.size.z;
        var vertnums = new Int16Array(nX * nY * nZ);
        var vals = data.data;

        var i, il;

        for (i = 0, il = vertnums.length; i < il; ++i)
            vertnums[i] = -1;

        var bitdata = new Uint8Array(nX * nY * nZ);

        //mark locations partitioned by isoval
        for (i = 0, il = vals.length; i < il; ++i) {
            var val = (isoval >= 0) ? vals[i] - isoval : isoval - vals[i];
            if (val > 0)
                bitdata[i] |= GLShape.ISDONE;

        }

        var verts = [], faces = [];
        MarchingCube.march(bitdata, verts, faces, {
            fulltable: true,
            voxel: voxel,
            unitCube: data.unit,
            origin: data.origin,
            matrix: data.matrix,
            nX: nX,
            nY: nY,
            nZ: nZ
        });

        if (!voxel && smoothness > 0)
            MarchingCube.laplacianSmooth(smoothness, verts, faces);
        var vertexmapping = [];
        var newvertices = [];
        var newfaces = [];

        if (volSpec.selectedRegion && volSpec.coords === undefined) {
            volSpec.coords = volSpec.selectedRegion; //backwards compat for incorrectly documented feature
        }
        if (volSpec.coords !== undefined) {

            var xmax = volSpec.coords[0].x,
                ymax = volSpec.coords[0].y,
                zmax = volSpec.coords[0].z,
                xmin = volSpec.coords[0].x,
                ymin = volSpec.coords[0].y,
                zmin = volSpec.coords[0].z;

            for (let i = 0; i < volSpec.coords.length; i++) {
                if (volSpec.coords[i].x > xmax)
                    xmax = volSpec.coords[i].x;
                else if (volSpec.coords[i].x < xmin)
                    xmin = volSpec.coords[i].x;
                if (volSpec.coords[i].y > ymax)
                    ymax = volSpec.coords[i].y;
                else if (volSpec.coords[i].y < ymin)
                    ymin = volSpec.coords[i].y;
                if (volSpec.coords[i].z > zmax)
                    zmax = volSpec.coords[i].z;
                else if (volSpec.coords[i].z < zmin)
                    zmin = volSpec.coords[i].z;
            }

            var rad = 2;
            if (volSpec.radius !== undefined) {
                rad = volSpec.radius; //backwards compat
            }
            if (volSpec.selectedOffset !== undefined) { //backwards compat
                rad = volSpec.selectedOffset;
            }
            if (volSpec.seldist !== undefined) {
                rad = volSpec.seldist;
            }

            xmin -= rad;
            xmax += rad;
            ymin -= rad;
            ymax += rad;
            zmin -= rad;
            zmax += rad;

            // accounts for radius
            for (let i = 0; i < verts.length; i++) {
                if (verts[i].x > xmin &&
                    verts[i].x < xmax &&
                    verts[i].y > ymin &&
                    verts[i].y < ymax &&
                    verts[i].z > zmin &&
                    verts[i].z < zmax &&
                    GLShape.inSelectedRegion(verts[i],
                        volSpec.coords, rad)) {
                    vertexmapping.push(newvertices.length);
                    newvertices.push(verts[i]);

                } else {
                    vertexmapping.push(-1);
                }

            }
            for (let i = 0; i + 2 < faces.length; i += 3) {
                if (vertexmapping[faces[i]] !== -1 &&
                    vertexmapping[faces[i + 1]] !== -1 &&
                    vertexmapping[faces[i + 2]] !== -1) {
                    newfaces.push(faces[i] - (faces[i] - vertexmapping[faces[i]]));
                    newfaces.push(faces[i + 1] - (faces[i + 1] - vertexmapping[faces[i + 1]]));
                    newfaces.push(faces[i + 2] - (faces[i + 2] - vertexmapping[faces[i + 2]]));
                }
            }
            verts = newvertices;
            faces = newfaces;
        }

        GLShape.drawCustom(this, this.geo, {
            vertexArr: verts,
            faceArr: faces,
            normalArr: [],
            clickable: volSpec.clickable,
            hoverable: volSpec.hoverable
        });

        this.updateStyle(volSpec);

        //computing bounding sphere from vertices
        var origin = new Vector3(data.origin.x, data.origin.y, data.origin.z);
        var size = new Vector3(data.size.x * data.unit.x, data.size.y * data.unit.y, data.size.z * data.unit.z);

        var total = new Vector3(0, 0, 0);
        var maxv = origin.clone();
        var minv = origin.clone().add(size);
        for (let i = 0; i < verts.length; i++) {
            total.add(verts[i]);
            maxv.max(verts[i]);
            minv.min(verts[i]);
        }
        total.divideScalar(verts.length);
        var len1 = total.distanceTo(minv);
        var len2 = total.distanceTo(maxv);
        this.boundingSphere.center = total;
        this.boundingSphere.radius = Math.max(len1, len2);
        if (typeof callback == "function")
            callback();
    };

    /** 
     * @deprecated Use addIsosurface instead
     * Creates custom shape from volumetric data 
     * @param {string} data - Volumetric input data 
     * @param {string} fmt - Input data format (e.g. 'cube' for cube file format)
     * @param {IsoSurfaceSpec} isoSpec - Volumetric data shape specification
     */
    public addVolumetricData(data, fmt, volSpec: IsoSurfaceSpec) {
        data = new VolumeData(data, fmt);
        this.addIsosurface(data, volSpec);
    };

    //for internal use, truncate buffers to save memory
    finalize() {
        GLShape.finalizeGeo(this.geo);
        this.geo.initTypedArrays();
        return this.geo;
    };

    /*
     * Initialize webgl objects for rendering
     * @param {$3Dmol.Object3D} group
     * 
     */
    globj(group) {

        if (this.renderedShapeObj) {
            group.remove(this.renderedShapeObj);
            this.renderedShapeObj = null;
        }

        if (this.hidden)
            return;
        GLShape.finalizeGeo(this.geo);
        this.geo.initTypedArrays();

        if (this.wireframe) {
            this.geo.setUpWireframe();
        }

        if (typeof (this.color) != 'undefined')
            GLShape.updateColor(this.geo, this.color);

        this.shapeObj = new Object3D();
        var material = null;

        if (this.side == DoubleSide) {
            material = new MeshDoubleLambertMaterial({
                wireframe: this.wireframe,
                side: this.side,
                transparent: (this.opacity < 1) ? true : false,
                opacity: this.opacity,
                wireframeLinewidth: this.linewidth,
                vertexColors: Coloring.VertexColors
            });
        } else {
            material = new MeshLambertMaterial({
                wireframe: this.wireframe,
                side: this.side,
                transparent: (this.opacity < 1) ? true : false,
                opacity: this.opacity,
                wireframeLinewidth: this.linewidth,
                vertexColors: Coloring.VertexColors
            });
        }

        var mesh = new Mesh(this.geo, material);

        this.shapeObj.add(mesh);

        var lineMaterial = new LineBasicMaterial({
            linewidth: this.linewidth,
            color: this.color
        });
        var line = new Line(this.linegeo, lineMaterial as Material, LineStyle.LinePieces);
        this.shapeObj.add(line);

        this.renderedShapeObj = this.shapeObj.clone();
        group.add(this.renderedShapeObj);

    };

    removegl(group) {
        if (this.renderedShapeObj) {
            // dispose of geos and materials
            if (this.renderedShapeObj.geometry !== undefined)
                this.renderedShapeObj.geometry.dispose();
            if (this.renderedShapeObj.material !== undefined)
                this.renderedShapeObj.material.dispose();
            group.remove(this.renderedShapeObj);
            this.renderedShapeObj = null;
        }
        this.shapeObj = null;
    };

    get position() {
        return this.boundingSphere.center;
    }

    get x() {
        return this.boundingSphere.center.x;
    }
    get y() {
        return this.boundingSphere.center.y;
    }
    get z() {
        return this.boundingSphere.center.z;
    }
};



export function splitMesh(mesh) {
    var MAXVERT = 64000; //webgl only supports 2^16 elements, leave a little breathing room (require at least 2)
    //peel off 64k vertices rsvh into their own mesh
    //duplicating vertices and normals as necessary to preserve faces and lines

    if (mesh.vertexArr.length < MAXVERT) return [mesh]; //typical case

    var slices: any = [{ vertexArr: [], normalArr: [], faceArr: [] }];
    if (mesh.colorArr) slices.colorArr = [];
    var vertSlice = []; //indexed by original vertex to get current slice
    var vertIndex = []; //indexed by original vertex to get index within slice
    var currentSlice = 0;

    //for each face, make sure all three vertices (or copies) are in the same slice
    var faces = mesh.faceArr;
    for (let i = 0, nf = faces.length; i < nf; i += 3) {
        let slice = slices[currentSlice];
        for (let j = 0; j < 3; j++) {
            //process each vertex to make sure it is assigned a slice
            //all vertices of a face must belong to the same slice
            var v = faces[i + j];
            if (vertSlice[v] !== currentSlice) { //true if undefined
                vertSlice[v] = currentSlice;
                vertIndex[v] = slice.vertexArr.length;
                slice.vertexArr.push(mesh.vertexArr[v]);
                if (mesh.normalArr && mesh.normalArr[v]) slice.normalArr.push(mesh.normalArr[v]);
                if (mesh.colorArr && mesh.colorArr[v]) slice.colorArr.push(mesh.colorArr[v]);
            }
            slice.faceArr.push(vertIndex[v]);
        }

        if (slice.vertexArr.length >= MAXVERT) {
            //new slice
            slices.push({ vertexArr: [], normalArr: [], faceArr: [] });
            if (mesh.colorArr) slices.colorArr = [];
            currentSlice++;
        }
    }
    return slices;
};

/**
 * GLShape style specification
 */
export interface ShapeSpec {
    /** Either a single color for the whole object or an array specifying the color at each vertex ({@link CustomShapeSpec}). */
    color?: ColorSpec | ColorSpec[];
    alpha?: number; //prefer opacity
    /** transparency, between 0 (invisible) and 1 (opaque) */
    opacity?: number;
    /** draw as wireframe, not surface */
    wireframe?: boolean;
    /** if true, do not display object */
    hidden?: boolean;
    /** width of line for wireframe rendering **No longer supported by most browsers** */
    linewidth?: number;
    /** if true, user can click on object to trigger callback */
    clickable?: boolean;
    /** function to call on click */
    callback?: Func;
    /** if true, user can hover on object to trigger callback */
    hoverable?: boolean;
    /** hover callback */
    hover_callback?: Func;
    /** unhover callback */
    unhover_callback?: Func;
    /** if set, only display in this frame of an animation */
    frame?: number;
    side?: number;
    voldata?: VolumeData;
    volscheme?: GradientType
};


/**
 * Isosurface style specification
 * @extends ShapeSpec
 */
export interface IsoSurfaceSpec extends ShapeSpec {
    /** specifies the isovalue to draw surface at */
    isoval?: number;
    /** if true uses voxel style rendering */
    voxel?: boolean;
    /** amount to smooth surface (default 1) */
    smoothness?: number;
    /** coordinates around which to include data; use viewer.selectedAtoms() to convert an AtomSelectionSpec to coordinates */
    coords?: XYZ[];
    /** distance around coords to include data [default = 2.0] */
    seldist?: number;
    /** volumetric data for vertex coloring, can be VolumeData object or raw data if volformat is specified */
    voldata?: VolumeData;
    /** coloring scheme for mapping volumetric data to vertex color, if not a Gradient object, show describe a builtin gradient one by providing an object with gradient, min, max, and (optionally) mid fields. */
    volscheme?: GradientType;
    /** format of voldata if not a $3Dmol.VolumeData object */
    volformat?: string;

    selectedRegion?: XYZ[]; //deprecated
    selectedOffset?: number; //deprecated
    radius?: number; //also deprecated
};



/**
 * Arrow shape specification.  
  * @extends ShapeSpec
 */
export interface ArrowSpec extends ShapeSpec {
    /** starting position */
    start?: XYZ;
    /** ending position */
    end?: XYZ;
    /** direction to extend from start (instead of specifying end) */
    dir?: XYZ;
    /** length to extend in dir direction from start (instead of specifying end) */
    length?: number;
    /** radius (default 0.1A) */
    radius?: number;
    /** color */
    color?: ColorSpec;
    /** hidden */
    hidden?: boolean;
    /** ratio of arrow base to cylinder (1.618034 default) */
    radiusRatio?: number;
    /** relative position of arrow base (0.618034 default) */
    mid?: number;
    /** position of arrow base in length units, if negative positioned from end instead of start.  Overrides mid. */
    midpos?: number;
};

/**
 * Cylinder shape specification.  
 * @extends ShapeSpec
 * 
 */
export interface CylinderSpec extends ShapeSpec {
    /** starting vector */
    start?: XYZ;
    /** ending position */
    end?: XYZ;
    /** radius */
    radius?: number;
    /** Place a cap at the start (none, flat or round) */
    fromCap?: CAP | string;
    /** Place a cap at the end (none, flat or round) */
    toCap?: CAP | string;
    /** Make the cylinder dashed. */
    dashed?: boolean;
    /** Length of dashes (default 0.25) */
    dashLength?: number;
    /** Length of gaps (default 0.25) */
    gapLength?: number;
};

/**
 * Curve shape specification.  
 * @extends ShapeSpec
 */
export interface CurveSpec extends ShapeSpec {
    /** Sequence of points to draw curve through */
    points?: XYZ[];
    /** amount of interpolation */
    smooth?: number;
    /** radius of curve */
    radius?: number;
    /** if an arrow should be drawn at the start */
    fromArrow?: boolean;
    /** if an arrow should be drawn at the end */
    toArrow?: boolean;
    /** have cap at start */
    fromCap?: CAP;
    /** have cap at end */
    toCap?: CAP;
};

/**
 * Line shape specification.  Default to wireframe.
 * @extends ShapeSpec
 */
export interface LineSpec extends ShapeSpec {
    /** Starting position */
    start?: XYZ;
    /** Ending position */
    end?: XYZ;
    /** make dashed */
    dashed?: boolean;
};

/**
 * Box shape specification. 
 * @extends ShapeSpec
 */
export interface BoxSpec extends ShapeSpec {
    /** bottom corner of box */
    corner?: XYZ;
    /** center of box */
    center?: XYZ;
    /** width, height, depth of box */
    dimensions?: {
        w: number | XYZ;
        h: number | XYZ;
        d: number | XYZ;
    };
};


/**
 * Specification for adding custom shape. 
 * @extends ShapeSpec
 */
export interface CustomShapeSpec extends ShapeSpec {
    /** List of vertex positions */
    vertexArr?: XYZ[];
    /** List of normal vectors for each vertex */
    normalArr?: XYZ[];
    /** List of triangles to build the custom shape. Each triangle is defined by the indices of 3 vertices in vertexArr, so the array length should be 3 times the number of faces. */
    faceArr?: number[];
};

/**
 * Sphere shape specification. Extends {@link ShapeSpec}.
 */
export interface SphereSpec extends ShapeSpec {
    /** center of sphere */
    center?: XYZ;
    /** radius of sphere */
    radius?: number;
    /** quality metric, higher uses more triangles (default 2) */
    quality?: number;
};