UNPKG

3dmol

Version:

JavaScript/TypeScript molecular visualization library

1,302 lines (1,108 loc) 50.5 kB
//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 { CAP, GLDraw } from "./GLDraw"; import { isNumeric, getColorFromStyle, extend } from "./utilities"; import { AtomSpec } from "specs"; /** * 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, /** If there is a gap of strictly fewer than gapcutoff missing residues * within a chain, draw a dashed line. */ gapcutoff?: 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; let disconnects = [] 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); } //check for disconnects in the same chain if (curr && next && !inConnectedResidues(curr, next) && curr.chain && curr.chain == next.chain && !curr.hetflag && !next.hetflag && curr.reschain + 1 == next.reschain) { disconnects.push([curr, next]); } 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); //cartoon gaps var drawGap = function (start: AtomSpec, end: AtomSpec) { if (start.style.cartoon.gapcutoff && start.style.cartoon.gapcutoff == end.style.cartoon.gapcutoff) { let cut = start.style.cartoon.gapcutoff; let gap = end.resi - start.resi; if (gap > 0 && gap < cut) { let radius = 0.25; let xdiff = end.x-start.x; let ydiff = end.y-start.y; let zdiff = end.z-start.z; let gapLength = Math.sqrt(xdiff*xdiff+ydiff*ydiff+zdiff*zdiff); let cylinderLength = gapLength/(2*gap); //one dash for each missing residue, evenly spaced let new_start = new Vector3(start.x,start.y,start.z); let div = gapLength/cylinderLength; let dashVector = new Vector3((end.x - start.x) / div, (end.y - start.y) / div, (end.z - start.z) / div); new_start.add({x:dashVector.x/2,y:dashVector.y/2,z:dashVector.z/2}); let new_end = new_start.clone().add(dashVector); dashVector.multiplyScalar(2); //skip over dash and gap let fakeres = extend({},start); for (var place = 0; place < gap