glcartoon.ts

//glcartoon.js
//This contains all the routines for rendering a cartoon given a set
//of atoms with assigned secondary structure

import { Vector3 } from "./WebGL/math";
import { Triangle, Sphere } from "./WebGL/shapes";
import { MeshDoubleLambertMaterial, Mesh, Geometry, Material, Coloring } from "./WebGL";
import { Gradient } from "./Gradient";
import { CC, ColorSpec, ColorschemeSpec } from "./colors";
import { GLDraw } from "./GLDraw";
import { isNumeric, getColorFromStyle } from "./utilities";

/**
 * A visualization of protein or nucleic acid secondary structure.  Applying this to other molecules will not show anything.
  In nucleic acids, the base cylinders obtain their color from the  atom to which the cylinder is drawn, which is 'N1' for purines (resn: 'A', 'G', 'DA', 'DG') and 
  'N3' for pyrimidines (resn: 'C', 'U', 'DC', 'DT'). 
  The different nucleobases can therefore be distinguished as by setting the colors 
  of each of these atoms. The backbone color is set from the 'P' atoms ('O5' for the 5' terminus).

 * 
 * @example $3Dmol.download("pdb:4ZD3",viewer,{},function(){
                  viewer.setBackgroundColor(0xffffffff);
                  viewer.setViewStyle({style:"outline"});
                  viewer.setStyle({},{cartoon:{}});
                  viewer.render();
              });
 */
export interface CartoonStyleSpec {
    /** do not show  */
    hidden?: boolean;
    /** colorscheme to use on atoms; overrides color */
    colorscheme?: ColorschemeSpec;    
    /** strand color, may specify as 'spectrum' which will apply reversed gradient based on residue number */
    color?: ColorSpec;
    /**  Allows the user to provide a function for setting the colorschemes. */
    colorfunc?: Function;    
    /**  style of cartoon rendering (trace, oval, rectangle (default), parabola, edged) */
    style?: string;
    /**  whether to use constant strand width, disregarding
    *       secondary structure; use thickness to adjust radius */
    ribbon?: boolean;
    /** whether to add arrows showing beta-sheet
    *       directionality; does not apply to trace or ribbon */
    arrows?: boolean;
    /** whether to display alpha helices as simple cylinders;
    *       does not apply to trace */
    tubes?: boolean;
    /** cartoon strand thickness, default is 0.4 */
    thickness?: number;
    /** cartoon strand width, default is secondary
    *       structure-dependent; does not apply to trace or ribbon */
    width?: number;
    /** set opacity from 0-1; transparency is set per-chain
    *       with a warning outputted in the event of ambiguity */
    opacity?: number
};

// helper functions
// Catmull-Rom subdivision
export function subdivide_spline(_points, DIV) { // points as Vector3
    var ret = [];
    var points = _points;
    points = []; // Smoothing test
    points.push(_points[0]);

    var i, lim, size;
    var p0, p1, p2, p3, v0, v1;

    for (i = 1, lim = _points.length - 1; i < lim; i++) {
        p1 = _points[i];
        p2 = _points[i + 1];
        if (p1.smoothen) {
            var np = new Vector3((p1.x + p2.x) / 2,
                (p1.y + p2.y) / 2, (p1.z + p2.z) / 2);
            np.atom = p1.atom;
            points.push(np);
        }
        else
            points.push(p1);
    }
    points.push(_points[_points.length - 1]);

    for (i = -1, size = points.length; i <= size - 3; i++) {
        p0 = points[(i === -1) ? 0 : i];
        p1 = points[i + 1];
        p2 = points[i + 2];
        p3 = points[(i === size - 3) ? size - 1 : i + 3];
        v0 = new Vector3().subVectors(p2, p0).multiplyScalar(0.5);
        v1 = new Vector3().subVectors(p3, p1).multiplyScalar(0.5);
        if (p2.skip)
            continue;

        for (var j = 0; j < DIV; j++) {
            var t = 1.0 / DIV * j;
            var x = p1.x + t * v0.x + t * t *
                (-3 * p1.x + 3 * p2.x - 2 * v0.x - v1.x) + t * t * t *
                (2 * p1.x - 2 * p2.x + v0.x + v1.x);
            var y = p1.y + t * v0.y + t * t *
                (-3 * p1.y + 3 * p2.y - 2 * v0.y - v1.y) + t * t * t *
                (2 * p1.y - 2 * p2.y + v0.y + v1.y);
            var z = p1.z + t * v0.z + t * t *
                (-3 * p1.z + 3 * p2.z - 2 * v0.z - v1.z) + t * t * t *
                (2 * p1.z - 2 * p2.z + v0.z + v1.z);

            var pt = new Vector3(x, y, z);
            if (j < DIV / 2) {
                pt.atom = p1.atom;
            } else {
                pt.atom = p2.atom;
            }

            ret.push(pt);
        }
    }
    ret.push(points[points.length - 1]);
    return ret;
};


const coilWidth = 0.5;
const helixSheetWidth = 1.3;
const nucleicAcidWidth = 0.8;
const defaultThickness = 0.4;
const baseThickness    = 0.4;

function drawThinStrip(geo: Geometry, p1, p2, colors) {

    var offset, vertoffset;
    var color, colori;

    for (var i = 0, lim = p1.length; i < lim; i++) {

        colori = Math.round(i * (colors.length - 1) / lim);
        color = CC.color(colors[colori]);

        var geoGroup = geo.updateGeoGroup(2);
        var vertexArray = geoGroup.vertexArray;
        var colorArray = geoGroup.colorArray;
        var faceArray = geoGroup.faceArray;
        offset = geoGroup.vertices;
        vertoffset = offset * 3;

        vertexArray[vertoffset] = p1[i].x;
        vertexArray[vertoffset + 1] = p1[i].y;
        vertexArray[vertoffset + 2] = p1[i].z;

        vertexArray[vertoffset + 3] = p2[i].x;
        vertexArray[vertoffset + 4] = p2[i].y;
        vertexArray[vertoffset + 5] = p2[i].z;

        for (var j = 0; j < 6; ++j) {
            colorArray[vertoffset + 3 * j] = color.r;
            colorArray[vertoffset + 1 + 3 * j] = color.g;
            colorArray[vertoffset + 2 + 3 * j] = color.b;
        }

        if (i > 0) {
            var faces = [offset, offset + 1, offset - 1, offset - 2];
            var faceoffset = geoGroup.faceidx;

            faceArray[faceoffset] = faces[0];
            faceArray[faceoffset + 1] = faces[1];
            faceArray[faceoffset + 2] = faces[3];
            faceArray[faceoffset + 3] = faces[1];
            faceArray[faceoffset + 4] = faces[2];
            faceArray[faceoffset + 5] = faces[3];

            geoGroup.faceidx += 6;
        }

        geoGroup.vertices += 2;
    }

};

function drawShapeStrip(geo: Geometry, points, colors, div, thickness, opacity, shape) {

    // points is a 2D array, dimensionality given by [num = cross-sectional
    // resolution][len = length of strip]
    var i, j, num, len;
    num = points.length;
    if (num < 2 || points[0].length < 2)
        return;

    for (i = 0; i < num; i++) { // spline to generate greater length-wise
        // resolution
        points[i] = subdivide_spline(points[i], div);
    }
    len = points[0].length;

    if (!thickness) // if thickness is 0, we can use a smaller geometry than
        // this function generates
        return drawThinStrip(geo, points[0], points[num - 1], colors);

    var axis, cs_shape, cs_bottom, cs_top, last_cs_bottom, last_cs_top;

    // cache the available cross-sectional shapes
    var cs_ellipse = [], cs_rectangle = [], cs_parabola = [];
    for (j = 0; j < num; j++) {
        cs_ellipse.push(0.25 + 1.5 *
            Math.sqrt((num - 1) * j - Math.pow(j, 2)) / (num - 1));
        cs_rectangle.push(0.5);
        cs_parabola.push(2 * (Math.pow(j / num, 2) - j / num) + 0.6);
    }

    /*
     * face_refs array is used to generate faces from vertexArray
     * iteratively. As we move through each cross-sectional segment of
     * points, we draw lateral faces backwards to the previous
     * cross-sectional segment.
     * 
     * To correctly identify the points needed to make each face we use this
     * array as a lookup table for the relative indices of each needed point
     * in the vertices array.
     * 
     * 4 points are used to create 2 faces.
     */

    var face_refs = [];
    for (j = 0; j < num * 2 - 1; j++) {
        /*
         * [curr vertex in curr cross-section, next vertex in curr
         * cross-section, next vertex in prev cross-section, curr vertex in
         * prev cross-section]
         */
        face_refs[j] = [j, j + 1, j + 1 - 2 * num, j - 2 * num];
    }
    // last face is different. easier to conceptualize this by drawing a
    // diagram
    face_refs[num * 2 - 1] = [j, j + 1 - 2 * num, j + 1 - 4 * num,
        j - 2 * num];

    var v_offset, va_offset, f_offset;
    var currentAtom;
    var color, colori;
    var vertexArray, colorArray, faceArray, face;
    let geoGroup = geo.updateGeoGroup();

    for (i = 0; i < len; i++) {
        let gnum = geo.groups;
        let replicating = false;
        geoGroup = geo.updateGeoGroup(2 * num); // ensure vertex capacity
        if (gnum != geo.groups && i > 0) {
            //we created a new geo - need to replicate vertices at edge 
            //(but not faces)
            i = i - 1;
            replicating = true;
        }
        colori = Math.round(i * (colors.length - 1) / len);
        color = CC.color(colors[colori]);

        last_cs_bottom = cs_bottom;
        last_cs_top = cs_top;
        cs_bottom = [];
        cs_top = [];
        axis = [];

        if (points[0][i].atom !== undefined) // TODO better edge case
        // handling
        {
            currentAtom = points[0][i].atom;
            if (shape === "oval")
                cs_shape = cs_ellipse;
            else if (shape === "rectangle")
                cs_shape = cs_rectangle;
            else if (shape === "parabola")
                cs_shape = cs_parabola;
        }
        if (!cs_shape)
            cs_shape = cs_rectangle;

        // calculate thickness at each width point, from cross-sectional
        // shape
        var toNext, toSide;
        for (j = 0; j < num; j++) {
            if (i < len - 1)
                toNext = points[j][i + 1].clone().sub(points[j][i]);
            else
                toNext = points[j][i - 1].clone().sub(points[j][i])
                    .negate();

            if (j < num - 1)
                toSide = points[j + 1][i].clone().sub(points[j][i]);
            else
                toSide = points[j - 1][i].clone().sub(points[j][i])
                    .negate();

            axis[j] = toSide.cross(toNext).normalize().multiplyScalar(
                thickness * cs_shape[j]);
        }

        // generate vertices by applying cross-sectional shape thickness to
        // input points
        for (j = 0; j < num; j++)
            cs_bottom[j] = points[j][i].clone().add(
                axis[j].clone().negate());
        for (j = 0; j < num; j++)
            cs_top[j] = points[j][i].clone().add(axis[j]);

        /*
         * Until this point the vertices have been dealt with as
         * Vector3() objects, but we need to serialize them into the
         * geoGroup.vertexArray, where every three indices represents the
         * next vertex. The colorArray is analogous.
         * 
         * In the following for-loops, j iterates through VERTICES so we
         * need to index them in vertexArray by 3*j + either 0, 1, or 2 for
         * xyz or rgb component.
         */

        vertexArray = geoGroup.vertexArray;
        colorArray = geoGroup.colorArray;
        faceArray = geoGroup.faceArray;
        v_offset = geoGroup.vertices;
        va_offset = v_offset * 3; // in case geoGroup already contains
        // vertices

        // bottom edge of cross-section, vertices [0, num)
        for (j = 0; j < num; j++) {
            vertexArray[va_offset + 3 * j + 0] = cs_bottom[j].x;
            vertexArray[va_offset + 3 * j + 1] = cs_bottom[j].y;
            vertexArray[va_offset + 3 * j + 2] = cs_bottom[j].z;
        }

        // top edge of cross-section, vertices [num, 2*num)
        // add these backwards, so that each cross-section's vertices are
        // added sequentially to vertexArray
        for (j = 0; j < num; j++) {
            vertexArray[va_offset + 3 * j + 0 + 3 * num] = cs_top[num - 1 - j].x;
            vertexArray[va_offset + 3 * j + 1 + 3 * num] = cs_top[num - 1 - j].y;
            vertexArray[va_offset + 3 * j + 2 + 3 * num] = cs_top[num - 1 - j].z;
        }

        for (j = 0; j < 2 * num; ++j) {
            colorArray[va_offset + 3 * j + 0] = color.r;
            colorArray[va_offset + 3 * j + 1] = color.g;
            colorArray[va_offset + 3 * j + 2] = color.b;
        }

        if (i > 0 && !replicating) {

            for (j = 0; j < num * 2; j++) {

                // get VERTEX indices of the 4 points of a rectangular face
                // (as opposed to literal vertexArray indices)
                face = [v_offset + face_refs[j][0],
                v_offset + face_refs[j][1],
                v_offset + face_refs[j][2],
                v_offset + face_refs[j][3]];

                f_offset = geoGroup.faceidx;

                // need 2 triangles to draw a face between 4 points
                faceArray[f_offset] = face[0];
                faceArray[f_offset + 1] = face[1];
                faceArray[f_offset + 2] = face[3];

                faceArray[f_offset + 3] = face[1];
                faceArray[f_offset + 4] = face[2];
                faceArray[f_offset + 5] = face[3];

                geoGroup.faceidx += 6;

                // TODO implement clickable the right way. midpoints of
                // strand between consecutive atoms
            }

            if (currentAtom.clickable || currentAtom.hoverable) {
                var faces = [];

                faces.push(new Triangle(last_cs_bottom[0],
                    cs_bottom[0], cs_bottom[num - 1]));
                faces.push(new Triangle(last_cs_bottom[0],
                    cs_bottom[num - 1], last_cs_bottom[num - 1]));

                faces.push(new Triangle(last_cs_bottom[num - 1],
                    cs_bottom[num - 1], cs_top[num - 1]));
                faces.push(new Triangle(last_cs_bottom[num - 1],
                    cs_top[num - 1], last_cs_top[num - 1]));

                faces.push(new Triangle(cs_top[0], last_cs_top[0],
                    last_cs_top[num - 1]));
                faces.push(new Triangle(cs_top[num - 1], cs_top[0],
                    last_cs_top[num - 1]));

                faces.push(new Triangle(cs_bottom[0],
                    last_cs_bottom[0], last_cs_top[0]));
                faces.push(new Triangle(cs_top[0], cs_bottom[0],
                    last_cs_top[0]));

                for (j in faces) {
                    currentAtom.intersectionShape.triangle.push(faces[j]);
                }
            }
        }

        geoGroup.vertices += 2 * num;
    }

    // for terminal faces
    vertexArray = geoGroup.vertexArray;
    colorArray = geoGroup.colorArray;
    faceArray = geoGroup.faceArray;
    v_offset = geoGroup.vertices;
    va_offset = v_offset * 3;
    f_offset = geoGroup.faceidx;

    for (i = 0; i < num - 1; i++) // "rear" face
    {
        face = [i, i + 1, 2 * num - 2 - i, 2 * num - 1 - i];

        f_offset = geoGroup.faceidx;

        faceArray[f_offset] = face[0];
        faceArray[f_offset + 1] = face[1];
        faceArray[f_offset + 2] = face[3];

        faceArray[f_offset + 3] = face[1];
        faceArray[f_offset + 4] = face[2];
        faceArray[f_offset + 5] = face[3];

        geoGroup.faceidx += 6;
    }

    for (i = 0; i < num - 1; i++) // "front" face
    {
        face = [v_offset - 1 - i, v_offset - 2 - i,
        v_offset - 2 * num + i + 1, v_offset - 2 * num + i];

        f_offset = geoGroup.faceidx;

        faceArray[f_offset] = face[0];
        faceArray[f_offset + 1] = face[1];
        faceArray[f_offset + 2] = face[3];

        faceArray[f_offset + 3] = face[1];
        faceArray[f_offset + 4] = face[2];
        faceArray[f_offset + 5] = face[3];

        geoGroup.faceidx += 6;
    }

};

function drawPlainStrip(geo, points, colors, div, thickness, opacity) {
    if ((points.length) < 2)
        return;

    var p1, p2;
    p1 = points[0];
    p2 = points[points.length - 1];

    p1 = subdivide_spline(p1, div);
    p2 = subdivide_spline(p2, div);
    if (!thickness)
        return drawThinStrip(geo, p1, p2, colors);

    // var vs = geo.vertices, fs = geo.faces;
    var vs = [];
    var axis, p1v, p2v, a1v, a2v;

    var faces = [[0, 2, -6, -8], [-4, -2, 6, 4], [7, -1, -5, 3],
    [-3, 5, 1, -7]];

    var offset, vertoffset, faceoffset;
    var color, colori;
    var currentAtom, lastAtom;
    var i, lim, j;
    var face1, face2, face3;
    var geoGroup, vertexArray, colorArray, faceArray;

    for (i = 0, lim = p1.length; i < lim; i++) {

        colori = Math.round(i * (colors.length - 1) / lim);
        color = CC.color(colors[colori]);

        vs.push(p1v = p1[i]); // 0
        vs.push(p1v); // 1
        vs.push(p2v = p2[i]); // 2
        vs.push(p2v); // 3
        if (i < lim - 1) {
            var toNext = p1[i + 1].clone().sub(p1[i]);
            var toSide = p2[i].clone().sub(p1[i]);
            axis = toSide.cross(toNext).normalize().multiplyScalar(
                thickness);
        }
        vs.push(a1v = p1[i].clone().add(axis)); // 4
        vs.push(a1v); // 5
        vs.push(a2v = p2[i].clone().add(axis)); // 6
        vs.push(a2v); // 7

        if (p1v.atom !== undefined)
            currentAtom = p1v.atom;

        geoGroup = geo.updateGeoGroup(8);
        vertexArray = geoGroup.vertexArray;
        colorArray = geoGroup.colorArray;
        faceArray = geoGroup.faceArray;
        offset = geoGroup.vertices;
        vertoffset = offset * 3;

        vertexArray[vertoffset] = p1v.x;
        vertexArray[vertoffset + 1] = p1v.y;
        vertexArray[vertoffset + 2] = p1v.z;
        vertexArray[vertoffset + 3] = p1v.x;
        vertexArray[vertoffset + 4] = p1v.y;
        vertexArray[vertoffset + 5] = p1v.z;
        vertexArray[vertoffset + 6] = p2v.x;
        vertexArray[vertoffset + 7] = p2v.y;
        vertexArray[vertoffset + 8] = p2v.z;
        vertexArray[vertoffset + 9] = p2v.x;
        vertexArray[vertoffset + 10] = p2v.y;
        vertexArray[vertoffset + 11] = p2v.z;
        vertexArray[vertoffset + 12] = a1v.x;
        vertexArray[vertoffset + 13] = a1v.y;
        vertexArray[vertoffset + 14] = a1v.z;
        vertexArray[vertoffset + 15] = a1v.x;
        vertexArray[vertoffset + 16] = a1v.y;
        vertexArray[vertoffset + 17] = a1v.z;
        vertexArray[vertoffset + 18] = a2v.x;
        vertexArray[vertoffset + 19] = a2v.y;
        vertexArray[vertoffset + 20] = a2v.z;
        vertexArray[vertoffset + 21] = a2v.x;
        vertexArray[vertoffset + 22] = a2v.y;
        vertexArray[vertoffset + 23] = a2v.z;

        for (j = 0; j < 8; ++j) {
            colorArray[vertoffset + 3 * j] = color.r;
            colorArray[vertoffset + 1 + 3 * j] = color.g;
            colorArray[vertoffset + 2 + 3 * j] = color.b;
        }

        if (i > 0) {

            // both points have distinct atoms
            var diffAtoms = ((lastAtom !== undefined && currentAtom !== undefined) && lastAtom.serial !== currentAtom.serial);

            for (j = 0; j < 4; j++) {

                var face = [offset + faces[j][0], offset + faces[j][1],
                offset + faces[j][2], offset + faces[j][3]];

                faceoffset = geoGroup.faceidx;

                faceArray[faceoffset] = face[0];
                faceArray[faceoffset + 1] = face[1];
                faceArray[faceoffset + 2] = face[3];
                faceArray[faceoffset + 3] = face[1];
                faceArray[faceoffset + 4] = face[2];
                faceArray[faceoffset + 5] = face[3];

                geoGroup.faceidx += 6;

                if (currentAtom.clickable || lastAtom.clickable || currentAtom.hoverable || lastAtom.hoverable) {

                    var p1a = vs[face[3]].clone(), p1b = vs[face[0]]
                        .clone(), p2a = vs[face[2]].clone(), p2b = vs[face[1]]
                            .clone();

                    p1a.atom = vs[face[3]].atom || null; // should be
                    // same
                    p2a.atom = vs[face[2]].atom || null;

                    p1b.atom = vs[face[0]].atom || null; // should be
                    // same
                    p2b.atom = vs[face[1]].atom || null;

                    if (diffAtoms) {
                        var m1 = p1a.clone().add(p1b).multiplyScalar(0.5);
                        var m2 = p2a.clone().add(p2b).multiplyScalar(0.5);
                        var m = p1a.clone().add(p2b).multiplyScalar(0.5);

                        if (j % 2 === 0) {
                            if (lastAtom.clickable || lastAtom.hoverable) {
                                face1 = new Triangle(m1, m, p1a);
                                face2 = new Triangle(m2, p2a, m);
                                face3 = new Triangle(m, p2a, p1a);
                                lastAtom.intersectionShape.triangle
                                    .push(face1);
                                lastAtom.intersectionShape.triangle
                                    .push(face2);
                                lastAtom.intersectionShape.triangle
                                    .push(face3);
                            }

                            if (currentAtom.clickable || currentAtom.hoverable) {
                                face1 = new Triangle(p1b, p2b, m);
                                face2 = new Triangle(p2b, m2, m);
                                face3 = new Triangle(p1b, m, m1);
                                currentAtom.intersectionShape.triangle
                                    .push(face1);
                                currentAtom.intersectionShape.triangle
                                    .push(face2);
                                currentAtom.intersectionShape.triangle
                                    .push(face3);
                            }
                        } else {
                            if (currentAtom.clickable || currentAtom.hoverable) {
                                face1 = new Triangle(m1, m, p1a);
                                face2 = new Triangle(m2, p2a, m);
                                face3 = new Triangle(m, p2a, p1a);
                                currentAtom.intersectionShape.triangle
                                    .push(face1);
                                currentAtom.intersectionShape.triangle
                                    .push(face2);
                                currentAtom.intersectionShape.triangle
                                    .push(face3);
                            }

                            if (lastAtom.clickable || lastAtom.hoverable) {
                                face1 = new Triangle(p1b, p2b, m);
                                face2 = new Triangle(p2b, m2, m);
                                face3 = new Triangle(p1b, m, m1);
                                lastAtom.intersectionShape.triangle
                                    .push(face1);
                                lastAtom.intersectionShape.triangle
                                    .push(face2);
                                lastAtom.intersectionShape.triangle
                                    .push(face3);
                            }
                        }

                    }

                    // face for single atom
                    else if (currentAtom.clickable || currentAtom.hoverable) {
                        face1 = new Triangle(p1b, p2b, p1a);
                        face2 = new Triangle(p2b, p2a, p1a);
                        currentAtom.intersectionShape.triangle.push(face1);
                        currentAtom.intersectionShape.triangle.push(face2);
                    }

                }

            }
        }

        geoGroup.vertices += 8;
        lastAtom = currentAtom;
    }

    var vsize = vs.length - 8; // Cap

    geoGroup = geo.updateGeoGroup(8);
    vertexArray = geoGroup.vertexArray;
    colorArray = geoGroup.colorArray;
    faceArray = geoGroup.faceArray;
    offset = geoGroup.vertices;
    vertoffset = offset * 3;
    faceoffset = geoGroup.faceidx;

    for (i = 0; i < 4; i++) {
        vs.push(vs[i * 2]);
        vs.push(vs[vsize + i * 2]);

        var v1 = vs[i * 2], v2 = vs[vsize + i * 2];

        vertexArray[vertoffset + 6 * i] = v1.x;
        vertexArray[vertoffset + 1 + 6 * i] = v1.y;
        vertexArray[vertoffset + 2 + 6 * i] = v1.z;
        vertexArray[vertoffset + 3 + 6 * i] = v2.x;
        vertexArray[vertoffset + 4 + 6 * i] = v2.y;
        vertexArray[vertoffset + 5 + 6 * i] = v2.z;

        colorArray[vertoffset + 6 * i] = color.r;
        colorArray[vertoffset + 1 + 6 * i] = color.g;
        colorArray[vertoffset + 2 + 6 * i] = color.b;
        colorArray[vertoffset + 3 + 6 * i] = color.r;
        colorArray[vertoffset + 4 + 6 * i] = color.g;
        colorArray[vertoffset + 5 + 6 * i] = color.b;

    }

    vsize += 8;

    face1 = [offset, offset + 2, offset + 6, offset + 4];
    face2 = [offset + 1, offset + 5, offset + 7, offset + 3];

    faceArray[faceoffset] = face1[0];
    faceArray[faceoffset + 1] = face1[1];
    faceArray[faceoffset + 2] = face1[3];
    faceArray[faceoffset + 3] = face1[1];
    faceArray[faceoffset + 4] = face1[2];
    faceArray[faceoffset + 5] = face1[3];
    faceArray[faceoffset + 6] = face2[0];
    faceArray[faceoffset + 7] = face2[1];
    faceArray[faceoffset + 8] = face2[3];
    faceArray[faceoffset + 9] = face2[1];
    faceArray[faceoffset + 10] = face2[2];
    faceArray[faceoffset + 11] = face2[3];

    geoGroup.faceidx += 12;
    geoGroup.vertices += 8;

    // TODO: Add intersection planes for caps
};


function drawStrip(geo, points, colors, div, thickness, opacity, shape) {
    if (!shape || shape === "default")
        shape = "rectangle";
    if (shape === 'edged')
        drawPlainStrip(geo, points, colors, div, thickness, opacity);
    else if (shape === "rectangle" || shape === "oval" || shape === "parabola")
        drawShapeStrip(geo, points, colors, div, thickness, opacity, shape);
};

// check if given atom is an alpha carbon
function isAlphaCarbon(atom) {
    return atom && atom.elem === "C" && atom.atom === "CA"; // note that
    // calcium is
    // also CA
};

// check whether two atoms are members of the same residue or subsequent,
// connected residues (a before b)
function inConnectedResidues(a, b) {
    if (a && b && a.chain === b.chain) {
        if (!a.hetflag && !b.hetflag && (a.reschain === b.reschain) &&
            (a.resi === b.resi || a.resi === b.resi - 1))
            return true;
        if (a.resi < b.resi) {
            // some PDBs have gaps in the numbering but the residues are
            // still connected
            // assume if within 4A they are connected
            var dx = a.x - b.x;
            var dy = a.y - b.y;
            var dz = a.z - b.z;
            var dist = dx * dx + dy * dy + dz * dz;
            if (a.atom == "CA" && b.atom == "CA" && dist < 16.0) //protein residues not connected
                return true; // calpha dist
            else if ((a.atom == "P" || b.atom == "P") && dist < 64.0) //dna
                return true;
        }
    }

    return false;
};

// add geo to the group
function setGeo(group, geo, opacity, outline, setNormals) {

    if (geo == null || geo.vertices == 0) return;
    if (setNormals) {
        geo.initTypedArrays();
        geo.setUpNormals();
    }

    var cartoonMaterial = new MeshDoubleLambertMaterial();
    cartoonMaterial.vertexColors = Coloring.FaceColors;
    if (typeof (opacity) === "number" && opacity >= 0 && opacity < 1) {
        cartoonMaterial.transparent = true;
        cartoonMaterial.opacity = opacity;
    }
    cartoonMaterial.outline = outline;
    var cartoonMesh = new Mesh(geo, cartoonMaterial as Material);
    group.add(cartoonMesh);
};

function addBackbonePoints(points, num, smoothen, backbonePt,
    orientPt, prevOrientPt, backboneAtom, atoms, atomi) {
    var widthScalar, i, delta, v, addArrowPoints, testStyle;

    if (!backbonePt || !orientPt || !backboneAtom)
        return;

    // the side vector points along the axis from backbone atom to
    // orientation atom (eg. CA to O, in peptides)
    var sideVec = orientPt.sub(backbonePt);
    sideVec.normalize();

    //find next atom like this one
    var forwardVec = atoms[atomi];
    for (i = atomi + 1; i < atoms.length; i++) {
        forwardVec = atoms[i];
        if (forwardVec.atom == backboneAtom.atom)
            break;
    }
    // the forward vector points along the axis from backbone atom to next
    // backbone atom
    forwardVec = forwardVec ? new Vector3(forwardVec.x,
        forwardVec.y, forwardVec.z) : new Vector3(0, 0, 0);
    forwardVec.sub(backbonePt);

    // adjustments for proper beta arrow appearance
    if (backboneAtom.ss === "arrow start") {
        var adjustment = forwardVec.clone().multiplyScalar(0.3).cross(
            orientPt); // adjust perpendicularly to strand face
        backbonePt.add(adjustment);

        var upVec = forwardVec.clone().cross(sideVec).normalize();
        sideVec.rotateAboutVector(upVec, 0.43);
    }

    // determine from cartoon style or secondary structure how wide the
    // strand should be here
    // ribbon shape should have same width as thickness
    if (backboneAtom.style.cartoon.ribbon) {
        widthScalar = backboneAtom.style.cartoon.thickness || defaultThickness;

    } else // depending on secondary structure, multiply the orientation
    // vector by some scalar
    {
        if (!backboneAtom.style.cartoon.width) {
            if (backboneAtom.ss === "c") {
                if (backboneAtom.atom === "P")
                    widthScalar = nucleicAcidWidth;
                else
                    widthScalar = coilWidth;
            } else if (backboneAtom.ss === "arrow start") {
                widthScalar = helixSheetWidth;
                addArrowPoints = true;

            } else if (backboneAtom.ss === "arrow end")
                widthScalar = coilWidth;

            else if (backboneAtom.ss === "h" &&
                backboneAtom.style.cartoon.tubes ||
                backboneAtom.ss === "tube start")
                widthScalar = coilWidth;

            else
                widthScalar = helixSheetWidth;
        } else
            widthScalar = backboneAtom.style.cartoon.width;
    }

    // make sure the strand orientation doesn't twist more than 90 degrees
    if (prevOrientPt != null && sideVec.dot(prevOrientPt) < 0)
        sideVec.negate();

    sideVec.multiplyScalar(widthScalar);
    for (i = 0; i < num; i++) {
        // produces NUM incremental points from backbone atom minus
        // orientation vector
        // to backbone atom plus orientation vector
        delta = -1 + i * 2 / (num - 1); // -1 to 1 incrementing by num
        v = new Vector3(backbonePt.x + delta * sideVec.x,
            backbonePt.y + delta * sideVec.y, backbonePt.z + delta * sideVec.z);
        v.atom = backboneAtom;
        if (smoothen && backboneAtom.ss === "s")
            v.smoothen = true;
        points[i].push(v); // a num-length array of arrays, where each
        // inner array contains length-wise points
        // along the backbone offset by some constant pertaining to its cell
        // in the outer array
    }

    if (addArrowPoints) {

        sideVec.multiplyScalar(2);
        for (i = 0; i < num; i++) {
            delta = -1 + i * 2 / (num - 1); // -1 to 1 incrementing by num
            v = new Vector3(backbonePt.x + delta * sideVec.x,
                backbonePt.y + delta * sideVec.y, backbonePt.z + delta * sideVec.z);
            v.atom = backboneAtom;
            v.smoothen = false;
            v.skip = true;
            points[i].push(v);
        }
    }

    // make sure the strand is all the same style
    testStyle = backboneAtom.style.cartoon.style || 'default';
    if (points.style) {
        if (points.style != testStyle) {
            console
                .log("Warning: a cartoon chain's strand-style is ambiguous");
            points.style = 'default';
        }

    } else
        points.style = testStyle;

    // revert ss keywords used for arrow rendering back to original value
    if (backboneAtom.ss === "arrow start" || backboneAtom.ss === "arrow end")
        backboneAtom.ss = "s";

    return addArrowPoints;
};

// proteins na backbone na terminus nucleobases
const cartoonAtoms = {
    "C": true, "CA": true, "O": true, "P": true, "OP2": true,
    "O2P": true, "O5'": true, "O3'": true, "C5'": true,
    "C2'": true, "O5*": true, "O3*": true, "C5*": true,
    "C2*": true, "N1": true, "N3": true
};
const purResns = { "DA": true, "DG": true, "A": true, "G": true };
const pyrResns = { "DT": true, "DC": true, "U": true, "C": true, "T": true };
const naResns = { "DA": true, "DG": true, "A": true, "G": true, "DT": true, "DC": true, "U": true, "C": true, "T": true };


export function drawCartoon(group, atomList, gradientrange, quality = 10) {

    let num = quality;
    let div = quality;

    var cartoon, prev, curr, next, currColor, nextColor, thickness, i;
    var backbonePt, orientPt, prevOrientPt, terminalPt, termOrientPt, baseStartPt, baseEndPt;
    var tubeStart, tubeEnd, drawingTube;
    var shapeGeo = new Geometry(true); // for shapes that don't need normals computed
    var geo = new Geometry(true);
    var colors = [];
    var points: any = [];
    var opacity = 1;
    var outline = false;

    var gradients: any = {};
    for (var g in Gradient.builtinGradients) {
        if (Gradient.builtinGradients.hasOwnProperty(g)) {
            //COUNTER INTUITIVE - spectrum reverses direction to gradient to match other tools
            gradients[g] = new Gradient.builtinGradients[g](gradientrange[1], gradientrange[0]);
        }
    }

    var cartoonColor = function (next, cartoon) { //atom and cartoon style object
        if (gradientrange && cartoon.color === 'spectrum') {
            if (cartoon.colorscheme in gradients) {
                return gradients[cartoon.colorscheme].valueToHex(next.resi);
            } else {
                return gradients.sinebow.valueToHex(next.resi);
            }
        }
        else {
            return getColorFromStyle(next, cartoon).getHex();
        }
    };

    for (i = 0; i < num; i++)
        points[i] = [];

    // first determine where beta sheet arrows and alpha helix tubes belong
    var inSheet = false;
    var inHelix = false; //only considering tube styled helices
    var atoms = [];
    for (i in atomList) {
        next = atomList[i];
        if (next.elem === 'C' && next.atom === 'CA') {
            var connected = inConnectedResidues(curr, next);

            // last two residues in a beta sheet become arrowhead
            if (connected && next.ss === "s") {
                inSheet = true;
            } else if (inSheet) {
                if (curr && prev && curr.style.cartoon.arrows && prev.style.cartoon.arrows) {
                    curr.ss = "arrow end";
                    prev.ss = "arrow start";
                }
                inSheet = false;
            }

            // first and last residues in a helix are used to draw tube
            if (connected && (curr.ss === "h" || curr.ss == "tube start") && curr.style.cartoon.tubes) {
                if (!inHelix && curr.ss != "tube start" && next.style.cartoon.tubes) {
                    next.ss = "tube start";
                    inHelix = true;
                }
            } else if (inHelix) {
                if (curr.ss === "tube start") {
                    curr.ss = "tube end"; //only one residue
                } else if (prev && prev.style.cartoon.tubes) {
                    prev.ss = "tube end";
                }
                inHelix = false;
            }
            prev = curr;
            curr = next;
        }
        if (next && next.atom in cartoonAtoms) {
            atoms.push(next);
        }
    }
    if (inHelix && curr.style.cartoon.tubes) {
        curr.ss = "tube end";
        inHelix = false;
    }

    var flushGeom = function (connect) {
        //write out points, update geom,etc
        if (points[0].length > 0) {
            drawStrip(geo, points, colors, div, thickness, opacity, points.style);
        }

        var saved = [], savedc = null;
        if (connect) {
            //recycle last point to first point of next points array
            for (i = 0; i < num; i++) {
                saved[i] = points[i][points[i].length - 1];
            }
            savedc = colors[colors.length - 1];
        }
        points = [];
        for (i = 0; i < num; i++)
            points[i] = [];
        colors = [];

        if (connect) {
            for (i = 0; i < num; i++) {
                points[i].push(saved[i]);
            }
            colors.push(savedc);
        }

        setGeo(group, geo, opacity, outline, true);
        setGeo(group, shapeGeo, opacity, outline, false);
        geo = new Geometry(true);
        shapeGeo = new Geometry(true);
    };

    // then accumulate points
    curr = undefined;
    for (var a = 0; a < atoms.length; a++) {
        next = atoms[a];

        var nextresn = next.resn.trim();
        var inNucleicAcid = nextresn in naResns;
        opacity = 1;
        // determine cartoon style
        cartoon = next.style.cartoon;
        if (curr && curr.style.cartoon)
            opacity = curr.style.cartoon.opacity;
        if (curr && curr.style.cartoon && curr.style.cartoon.outline)
            outline = curr.style.cartoon.outline;

        // create a new geometry when opacity changes
        //this should work fine if opacity is set by chain, but will
        //break if it changes within the chain
        if (curr && curr.style.cartoon && (!next.style.cartoon ||
            curr.style.cartoon.opacity != next.style.cartoon.opacity)) {
            flushGeom(curr.chain == next.chain);
        }

        if (cartoon.style === "trace") // draw cylinders connecting
        // consecutive 'backbone' atoms
        {
            /*
             * "trace" style just draws cylinders between consecutive
             * 'backbone' atoms, such as alpha carbon for polypeptides and
             * phosphorus for DNA.
             */
            if (next.hetflag) {
                ; //ignore non-protein atoms
            } else if (next.elem === 'C' && next.atom === 'CA' ||
                inNucleicAcid && next.atom === "P" ||
                next.atom === 'BB') {
                // determine cylinder color
                nextColor = cartoonColor(next, cartoon);

                // determine cylinder thickness
                if (isNumeric(cartoon.thickness))
                    thickness = cartoon.thickness;
                else
                    thickness = defaultThickness;

                if (inConnectedResidues(curr, next)) {
                    // if both atoms are same color, draw single cylinder
                    if (nextColor == currColor) {
                        var color = CC.color(nextColor);
                        GLDraw.drawCylinder(shapeGeo, curr, next,
                            thickness, color, 2, 2);
                    }

                    else // otherwise draw cylinders for each color
                    // (split down the middle)
                    {
                        var midpoint = new Vector3().addVectors(
                            curr, next).multiplyScalar(0.5);
                        var color1 = CC.color(currColor);
                        var color2 = CC.color(nextColor);
                        GLDraw.drawCylinder(shapeGeo, curr,
                            midpoint, thickness, color1, 2, 0);
                        GLDraw.drawCylinder(shapeGeo, midpoint,
                            next, thickness, color2, 0, 2);
                    } // note that an atom object can be duck-typed as a
                    // Vector3 in this case
                }

                if ((next.clickable === true || next.hoverable) && (next.intersectionShape !== undefined)) {
                    //can click on joints to get alpha carbons
                    var center = new Vector3(next.x, next.y, next.z);
                    next.intersectionShape.sphere.push(new Sphere(center, thickness));
                }

                curr = next;
                currColor = nextColor;
            }
        } else // draw default-style cartoons based on secondary structure
        {
            // draw backbone through these atoms
            if (isAlphaCarbon(next) || inNucleicAcid && (next.atom === "P" || next.atom.indexOf('O5') == 0)) {
                if (drawingTube) {
                    if (next.ss === "tube end") {
                        drawingTube = false;
                        tubeEnd = new Vector3(next.x, next.y, next.z);
                        GLDraw.drawCylinder(shapeGeo, tubeStart,
                            tubeEnd, 2, CC.color(currColor), 1,
                            1);
                        next.ss = "h";

                    }
                    else if (curr.chain != next.chain || curr.ss === "tube end") { //don't span chains no matter what, check for short tubes (less than ideal)
                        drawingTube = false;
                        curr.ss = "h";
                        tubeEnd = new Vector3(curr.x, curr.y, curr.z);
                        GLDraw.drawCylinder(shapeGeo, tubeStart,
                            tubeEnd, 2, CC.color(currColor), 1,
                            1);
                    }
                    else
                        continue; // don't accumulate strand points while
                    // in the middle of drawing a tube
                }

                // end of a chain of connected residues (of same style)
                if (curr && (!inConnectedResidues(curr, next) || curr.ss === "tube start")) {
                    if (curr.ss === "tube start") {
                        drawingTube = true;
                        tubeStart = new Vector3(curr.x, curr.y,
                            curr.z);
                        curr.ss = "h";
                    }

                    if (baseEndPt) // draw the last base if it's a NA chain
                    {
                        if (terminalPt)
                            baseStartPt = new Vector3().addVectors(
                                curr, terminalPt).multiplyScalar(0.5);
                        else
                            baseStartPt = new Vector3(curr.x,
                                curr.y, curr.z);

                        GLDraw.drawCylinder(shapeGeo, baseStartPt,
                            baseEndPt, baseThickness, CC
                                .color(baseEndPt.color), 0, 2);
                        addBackbonePoints(points, num,
                            true, terminalPt, termOrientPt,
                            prevOrientPt, curr, atoms, a);
                        colors.push(nextColor);

                        baseStartPt = null;
                        baseEndPt = null;
                    }

                    // draw accumulated strand points
                    if (points[0].length > 0)
                        drawStrip(geo, points, colors, div, thickness,
                            opacity, points.style);

                    // clear arrays for points and colors
                    points = [];
                    for (i = 0; i < num; i++)
                        points[i] = [];
                    colors = [];
                }

                // reached next residue (potentially the first residue)
                if (curr === undefined || curr.rescode != next.rescode || curr.resi != next.resi) {
                    if (baseEndPt && curr != undefined) // draw last NA residue's base
                    {
                        // start the cylinder at the midpoint between
                        // consecutive backbone atoms
                        baseStartPt = new Vector3().addVectors(curr,
                            next).multiplyScalar(0.5);
                        var startFix = baseStartPt.clone().sub(baseEndPt)
                            .multiplyScalar(0.02); // TODO: apply this
                        // as function of
                        // thickness
                        baseStartPt.add(startFix);

                        GLDraw.drawCylinder(shapeGeo, baseStartPt,
                            baseEndPt, baseThickness, CC
                                .color(baseEndPt.color), 0, 2);
                        baseStartPt = null;
                        baseEndPt = null;
                    }

                    // determine color and thickness of the next strand
                    // segment
                    nextColor = cartoonColor(next, cartoon);
                    colors.push(nextColor);
                    if (isNumeric(cartoon.thickness))
                        thickness = cartoon.thickness;
                    else
                        thickness = defaultThickness;

                    curr = next; // advance backbone
                    backbonePt = new Vector3(curr.x, curr.y, curr.z);
                    backbonePt.resi = curr.resi;
                    currColor = nextColor;
                }

                // click handling
                if ((next.clickable === true || next.hoverable === true) &&
                    (next.intersectionShape === undefined || next.intersectionShape.triangle === undefined))
                    next.intersectionShape = {
                        sphere: null,
                        cylinder: [],
                        line: [],
                        triangle: []
                    };
            }
            // atoms used to orient the backbone strand
            else if (curr != undefined && (isAlphaCarbon(curr) && next.atom === "O" ||
                inNucleicAcid && curr.atom === "P" &&
                (next.atom === "OP2" || next.atom === "O2P") ||
                inNucleicAcid && curr.atom.indexOf("O5") == 0 &&
                next.atom.indexOf("C5") == 0)) {
                orientPt = new Vector3(next.x, next.y, next.z);
                orientPt.resi = next.resi;
                if (next.atom === "OP2" || next.atom === "O2P") // for NA 3'
                    // terminus
                    termOrientPt = new Vector3(next.x, next.y,
                        next.z);
            }

            // NA 3' terminus is an edge case, need a vector for most recent
            // O3'
            else if (inNucleicAcid && next.atom.indexOf("O3") == 0) {
                terminalPt = new Vector3(next.x, next.y, next.z);
            }

            // atoms used for drawing the NA base cylinders (diff for
            // purines and pyramidines)
            else if ((next.atom === "N1" && (nextresn in purResns)) ||
                (next.atom === "N3" && (nextresn in pyrResns))) {
                baseEndPt = new Vector3(next.x, next.y, next.z);
                baseEndPt.color = getColorFromStyle(next, cartoon)
                    .getHex();
            }

            // when we have a backbone point and orientation point in the
            // same residue, accumulate strand points
            if (orientPt && backbonePt && orientPt.resi === backbonePt.resi) {
                addBackbonePoints(points, num, true,
                    backbonePt, orientPt, prevOrientPt, curr, atoms,
                    a);
                prevOrientPt = orientPt;
                backbonePt = null;
                orientPt = null;
                colors.push(nextColor);
            }
        }

    }

    if (baseEndPt) // draw last NA base if needed
    {
        if (terminalPt)
            baseStartPt = new Vector3().addVectors(curr, terminalPt)
                .multiplyScalar(0.5);
        else
            baseStartPt = new Vector3(curr.x, curr.y, curr.z);

        GLDraw.drawCylinder(shapeGeo, baseStartPt, baseEndPt, baseThickness,
            CC.color(baseEndPt.color), 0, 2);
        addBackbonePoints(points, num, true, terminalPt,
            termOrientPt, prevOrientPt, curr, atoms, a);
        colors.push(nextColor);
    }

    // for default style, draw the last strand
    flushGeom(false);
};