// A model is a collection of related atoms. Bonds are only allowed between //atoms in the same model. An atom is uniquely specified by its model id and //its serial number. //A glmodel knows how to apply the styles on each atom to create a gl object import { Geometry, Material, StickImposterMaterial } from "./WebGL"; import { Sphere, Cylinder } from "./WebGL/shapes"; import { Vector3, Matrix4, conversionMatrix3, Matrix3, XYZ } from "./WebGL/math"; import { Color, CC, ColorschemeSpec, ColorSpec } from "./colors"; import { InstancedMaterial, SphereImposterMaterial, MeshLambertMaterial, Object3D, Mesh, LineBasicMaterial, Line, LineStyle } from "./WebGL"; import { CAP, GLDraw } from "./GLDraw" import { CartoonStyleSpec, drawCartoon } from "./glcartoon"; import { elementColors } from "./colors"; import { get, deepCopy, extend, getExtent, getAtomProperty, makeFunction, getPropertyRange, specStringToObject, getbin, getColorFromStyle } from "./utilities"; import { Gradient } from "./Gradient"; import { Parsers } from "./parsers"; import { NetCDFReader } from "netcdfjs" import { inflate, InflateFunctionOptions, Data } from "pako" import { AtomSelectionSpec, AtomSpec } from "./specs"; import { GLViewer } from "GLViewer"; import { ArrowSpec } from "GLShape"; import { ParserOptionsSpec } from "./parsers/ParserOptionsSpec"; import { LabelSpec } from "Label"; import { assignBonds } from "./parsers/utils/assignBonds"; function inflateString(str: string | ArrayBuffer): (string | ArrayBuffer) { let data: Data; if (typeof str === 'string') { const encoder = new TextEncoder(); data = encoder.encode(str); } else { data = new Uint8Array(str); } const inflatedData = inflate(data, { to: 'string' } as InflateFunctionOptions & { to: 'string' }); return inflatedData; } /** * GLModel represents a group of related atoms * @class */ export class GLModel { // class variables go here static defaultAtomStyle: AtomStyleSpec = { line: {} }; static defaultlineWidth = 1.0; // Reference: A. Bondi, J. Phys. Chem., 1964, 68, 441. // https://en.wikipedia.org/wiki/Van_der_Waals_radius static vdwRadii = { "H": 1.2, "He": 1.4, "Li": 1.82, "Be": 1.53, "B": 1.92, "C": 1.7, "N": 1.55, "O": 1.52, "F": 1.47, "Ne": 1.54, "Na": 2.27, "Mg": 1.73, "Al": 1.84, "Si": 2.1, "P": 1.8, "S": 1.8, "Cl": 1.75, "Ar": 1.88, "K": 2.75, "Ca": 2.31, "Ni": 1.63, "Cu": 1.4, "Zn": 1.39, "Ga": 1.87, "Ge": 2.11, "As": 1.85, "Se": 1.9, "Br": 1.85, "Kr": 2.02, "Rb": 3.03, "Sr": 2.49, "Pd": 1.63, "Ag": 1.72, "Cd": 1.58, "In": 1.93, "Sn": 2.17, "Sb": 2.06, "Te": 2.06, "I": 1.98, "Xe": 2.16, "Cs": 3.43, "Ba": 2.68, "Pt": 1.75, "Au": 1.66, "Hg": 1.55, "Tl": 1.96, "Pb": 2.02, "Bi": 2.07, "Po": 1.97, "At": 2.02, "Rn": 2.20, "Fr": 3.48, "Ra": 2.83, "U": 1.86 }; // class functions // return true if a and b represent the same style static sameObj(a, b) { if (a && b) return JSON.stringify(a) == JSON.stringify(b); else return a == b; }; public unitCellObjects: any; // private variables private atoms: AtomSpec[] = []; private frames: any = []; private box: any = null; private atomdfs: any = null; //depth first search over connected components private id = 0; private hidden: any = false; private molObj: any = null; private renderedMolObj: any = null; private lastColors: any = null; private modelData: any = {}; private modelDatas: any = null; //if there is different modelData per frame private idMatrix = new Matrix4(); private dontDuplicateAtoms = true; private defaultColor = elementColors.defaultColor; private options: any; private ElementColors: any; private readonly defaultSphereRadius: number; private readonly defaultCartoonQuality: number; // bonds as cylinders private readonly defaultStickRadius = 0.25; constructor(mid, options?) { this.options = options || {}; this.ElementColors = (this.options.defaultcolors) ? this.options.defaultcolors : elementColors.defaultColors; this.defaultSphereRadius = (this.options.defaultSphereRadius) ? this.options.defaultSphereRadius : 1.5; this.defaultCartoonQuality = (this.options.cartoonQuality) ? this.options.cartoonQuality : 10; this.id = mid; } // return proper radius for atom given style /** * * @param {AtomSpec} atom * @param {atomstyle} style * @return {number} * */ private getRadiusFromStyle(atom:AtomSpec, style:SphereStyleSpec|ClickSphereStyleSpec|CrossStyleSpec) { var r = this.defaultSphereRadius; if (typeof (style.radius) != "undefined") r = style.radius; else if (GLModel.vdwRadii[atom.elem]) r = GLModel.vdwRadii[atom.elem]; else if (atom.elem.length > 1) { //see if adjusting case helps let e: string = atom.elem; e = e[0].toUpperCase() + e[1].toLowerCase(); if (GLModel.vdwRadii[e]) r = GLModel.vdwRadii[e]; } if (typeof (style.scale) != "undefined") r *= style.scale; return r; }; // cross drawing /** * * @param {AtomSpec} atom * @param {Record} geos */ private drawAtomCross(atom:AtomSpec, geos: Record) { if (!atom.style.cross) return; var style = atom.style.cross; if (style.hidden) return; var linewidth = (style.linewidth || GLModel.defaultlineWidth); if (!geos[linewidth]) geos[linewidth] = new Geometry(); var geoGroup = geos[linewidth].updateGeoGroup(6); var delta = this.getRadiusFromStyle(atom, style); var points = [[delta, 0, 0], [-delta, 0, 0], [0, delta, 0], [0, -delta, 0], [0, 0, delta], [0, 0, -delta]]; var clickable = atom.clickable || atom.hoverable; if (clickable && atom.intersectionShape === undefined) atom.intersectionShape = { sphere: [], cylinder: [], line: [] }; var c = getColorFromStyle(atom, style); var vertexArray = geoGroup.vertexArray; var colorArray = geoGroup.colorArray; for (var j = 0; j < 6; j++) { var offset = geoGroup.vertices * 3; geoGroup.vertices++; vertexArray[offset] = atom.x + points[j][0]; vertexArray[offset + 1] = atom.y + points[j][1]; vertexArray[offset + 2] = atom.z + points[j][2]; colorArray[offset] = c.r; colorArray[offset + 1] = c.g; colorArray[offset + 2] = c.b; if (clickable) { var point = new Vector3(points[j][0], points[j][1], points[j][2]); //decrease cross size for selection to prevent misselection from atom overlap point.multiplyScalar(0.1); point.set(point.x + atom.x, point.y + atom.y, point.z + atom.z); atom.intersectionShape.line.push(point); } } }; private getGoodCross(atom:AtomSpec, atom2:AtomSpec, p1, dir) { // get vector 2 different neighboring atom //find most divergent neighbor var bestv = null; var bestlen = -1; for (var j = 0, n = atom.bonds.length; j < n; j++) { if (atom.bonds[j] != atom2.index) { let j2 = atom.bonds[j]; let atom3 = this.atoms[j2]; let p3 = new Vector3(atom3.x, atom3.y, atom3.z); let dir2 = p3.clone(); dir2.sub(p1); let v = dir2.clone(); v.cross(dir); var l = v.lengthSq(); if (l > bestlen) { bestlen = l; bestv = v; if (bestlen > 0.1) { return bestv; } } } } return bestv; }; //from atom, return a normalized vector v that is orthogonal and along which //it is appropraite to draw multiple bonds private getSideBondV(atom:AtomSpec, atom2:AtomSpec, i:number) { var i2, j2, atom3, p3, dir2; var p1 = new Vector3(atom.x, atom.y, atom.z); var p2 = new Vector3(atom2.x, atom2.y, atom2.z); var dir = p2.clone(); var v = null; dir.sub(p1); if (atom.bonds.length === 1) { if (atom2.bonds.length === 1) { v = dir.clone(); if (Math.abs(v.x) > 0.0001) v.y += 1; else v.x += 1; } else { i2 = (i + 1) % atom2.bonds.length; j2 = atom2.bonds[i2]; atom3 = this.atoms[j2]; if(atom3.index == atom.index) { // get distinct atom i2 = (i2 + 1) % atom2.bonds.length; j2 = atom2.bonds[i2]; atom3 = this.atoms[j2]; } p3 = new Vector3(atom3.x, atom3.y, atom3.z); dir2 = p3.clone(); dir2.sub(p1); v = dir2.clone(); v.cross(dir); } } else { v = this.getGoodCross(atom, atom2, p1, dir); if (v.lengthSq() < 0.01) { var v2 = this.getGoodCross(atom2, atom, p1, dir); if (v2 != null) v = v2; //can be null if no other neighbors } } // especially for C#C (triple bond) dir and dir2 // may be opposites resulting in a zero v if (v.lengthSq() < 0.01) { v = dir.clone(); if (Math.abs(v.x) > 0.0001) v.y += 1; else v.x += 1; } v.cross(dir); v.normalize(); return v; }; private addLine(vertexArray, colorArray, offset, p1: Vector3, p2: Vector3, c1: Color) { //make line from p1 to p2, does not incremeant counts vertexArray[offset] = p1.x; vertexArray[offset + 1] = p1.y; vertexArray[offset + 2] = p1.z; colorArray[offset] = c1.r; colorArray[offset + 1] = c1.g; colorArray[offset + 2] = c1.b; vertexArray[offset + 3] = p2.x; vertexArray[offset + 4] = p2.y; vertexArray[offset + 5] = p2.z; colorArray[offset + 3] = c1.r; colorArray[offset + 4] = c1.g; colorArray[offset + 5] = c1.b; }; // bonds - both atoms must match bond style // standardize on only drawing for lowest to highest /** * * @param {AtomSpec} * atom * @param {AtomSpec[]} atoms * @param {Record} geos */ private drawBondLines(atom:AtomSpec, atoms:AtomSpec[], geos: Record) { if (!atom.style.line) return; var style = atom.style.line; if (style.hidden) return; var p1a, p1b, p2a, p2b; // have a separate geometry for each linewidth var linewidth = (style.linewidth || GLModel.defaultlineWidth); if (!geos[linewidth]) geos[linewidth] = new Geometry(); /** @type {geometryGroup} */ var geoGroup = geos[linewidth].updateGeoGroup(6 * atom.bonds.length); //reserve enough space even for triple bonds var vertexArray = geoGroup.vertexArray; var colorArray = geoGroup.colorArray; for (var i = 0; i < atom.bonds.length; i++) { var j = atom.bonds[i]; // our neighbor var atom2 = atoms[j]; if (!atom2.style.line) continue; // don't sweat the details if (atom.index >= atom2.index) // only draw if less, this way we can do multi bonds correctly continue; var p1 = new Vector3(atom.x, atom.y, atom.z); var p2 = new Vector3(atom2.x, atom2.y, atom2.z); var mp = p1.clone().add(p2).multiplyScalar(0.5); var singleBond = false; var atomneedsi = atom.clickable || atom.hoverable; var atom2needsi = atom2.clickable || atom2.hoverable; if (atomneedsi || atom2needsi) { if (atomneedsi) { if (atom.intersectionShape === undefined) atom.intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; atom.intersectionShape.line.push(p1); atom.intersectionShape.line.push(mp); } if (atom2needsi) { if (atom2.intersectionShape === undefined) atom2.intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; atom2.intersectionShape.line.push(mp); atom2.intersectionShape.line.push(p2); } } var c1 = getColorFromStyle(atom, atom.style.line); var c2 = getColorFromStyle(atom2, atom2.style.line); if (atom.bondStyles && atom.bondStyles[i]) { var bstyle = atom.bondStyles[i]; if (!bstyle.iswire) { continue; } if (bstyle.singleBond) singleBond = true; if (typeof (bstyle.color1) != "undefined") { c1 = CC.color(bstyle.color1) as Color; } if (typeof (bstyle.color2) != "undefined") { c2 = CC.color(bstyle.color2) as Color; } } var offset = geoGroup.vertices * 3; var mpa, mpb; if (atom.bondOrder[i] > 1 && atom.bondOrder[i] < 4 && !singleBond) { var v = this.getSideBondV(atom, atom2, i); var dir = p2.clone(); dir.sub(p1); if (atom.bondOrder[i] == 2) { //double v.multiplyScalar(0.1); p1a = p1.clone(); p1a.add(v); p1b = p1.clone(); p1b.sub(v); p2a = p1a.clone(); p2a.add(dir); p2b = p1b.clone(); p2b.add(dir); if (c1 == c2) { geoGroup.vertices += 4; this.addLine(vertexArray, colorArray, offset, p1a, p2a, c1); this.addLine(vertexArray, colorArray, offset + 6, p1b, p2b, c1); } else { geoGroup.vertices += 8; dir.multiplyScalar(0.5); mpa = p1a.clone(); mpa.add(dir); mpb = p1b.clone(); mpb.add(dir); this.addLine(vertexArray, colorArray, offset, p1a, mpa, c1); this.addLine(vertexArray, colorArray, offset + 6, mpa, p2a, c2); this.addLine(vertexArray, colorArray, offset + 12, p1b, mpb, c1); this.addLine(vertexArray, colorArray, offset + 18, mpb, p2b, c2); } } else if (atom.bondOrder[i] == 3) { //triple v.multiplyScalar(0.1); p1a = p1.clone(); p1a.add(v); p1b = p1.clone(); p1b.sub(v); p2a = p1a.clone(); p2a.add(dir); p2b = p1b.clone(); p2b.add(dir); if (c1 == c2) { geoGroup.vertices += 6; this.addLine(vertexArray, colorArray, offset, p1, p2, c1); this.addLine(vertexArray, colorArray, offset + 6, p1a, p2a, c1); this.addLine(vertexArray, colorArray, offset + 12, p1b, p2b, c1); } else { geoGroup.vertices += 12; dir.multiplyScalar(0.5); mpa = p1a.clone(); mpa.add(dir); mpb = p1b.clone(); mpb.add(dir); this.addLine(vertexArray, colorArray, offset, p1, mp, c1); this.addLine(vertexArray, colorArray, offset + 6, mp, p2, c2); this.addLine(vertexArray, colorArray, offset + 12, p1a, mpa, c1); this.addLine(vertexArray, colorArray, offset + 18, mpa, p2a, c2); this.addLine(vertexArray, colorArray, offset + 24, p1b, mpb, c1); this.addLine(vertexArray, colorArray, offset + 30, mpb, p2b, c2); } } } else { //single bond if (c1 == c2) { geoGroup.vertices += 2; this.addLine(vertexArray, colorArray, offset, p1, p2, c1); } else { geoGroup.vertices += 4; this.addLine(vertexArray, colorArray, offset, p1, mp, c1); this.addLine(vertexArray, colorArray, offset + 6, mp, p2, c2); } } } }; //sphere drawing //See also: drawCylinder /** * * @param {AtomSpec} atom * @param {Geometry} geo */ private drawAtomSphere(atom: AtomSpec, geo: Geometry) { if (!atom.style.sphere) return; var style = atom.style.sphere; if (style.hidden) return; var C = getColorFromStyle(atom, style); var radius = this.getRadiusFromStyle(atom, style); if ((atom.clickable === true || atom.hoverable) && (atom.intersectionShape !== undefined)) { var center = new Vector3(atom.x, atom.y, atom.z); atom.intersectionShape.sphere.push(new Sphere(center, radius)); } GLDraw.drawSphere(geo, atom, radius, C); }; /** Register atom shaped click handlers */ private drawAtomClickSphere(atom: AtomSpec) { if (!atom.style.clicksphere) return; var style = atom.style.clicksphere; if (style.hidden) return; var radius = this.getRadiusFromStyle(atom, style); if ((atom.clickable === true || atom.hoverable) && (atom.intersectionShape !== undefined)) { var center = new Vector3(atom.x, atom.y, atom.z); atom.intersectionShape.sphere.push(new Sphere(center, radius)); } }; private drawAtomInstanced(atom: AtomSpec, geo: Geometry) { if (!atom.style.sphere) return; var style = atom.style.sphere; if (style.hidden) return; var radius = this.getRadiusFromStyle(atom, style); var C = getColorFromStyle(atom, style); var geoGroup = geo.updateGeoGroup(1); var startv = geoGroup.vertices; var start = startv * 3; var vertexArray = geoGroup.vertexArray; var colorArray = geoGroup.colorArray; var radiusArray = geoGroup.radiusArray; vertexArray[start] = atom.x; vertexArray[start + 1] = atom.y; vertexArray[start + 2] = atom.z; colorArray[start] = C.r; colorArray[start + 1] = C.g; colorArray[start + 2] = C.b; radiusArray[startv] = radius; if ((atom.clickable === true || atom.hoverable) && (atom.intersectionShape !== undefined)) { var center = new Vector3(atom.x, atom.y, atom.z); atom.intersectionShape.sphere.push(new Sphere(center, radius)); } geoGroup.vertices += 1; }; private drawSphereImposter(geo: Geometry, center: XYZ, radius: number, C: Color) { //create flat square var geoGroup = geo.updateGeoGroup(4); var i; var startv = geoGroup.vertices; var start = startv * 3; var vertexArray = geoGroup.vertexArray; var colorArray = geoGroup.colorArray; //use center point for each vertex for (i = 0; i < 4; i++) { vertexArray[start + 3 * i] = center.x; vertexArray[start + 3 * i + 1] = center.y; vertexArray[start + 3 * i + 2] = center.z; } //same colors for all 4 vertices var normalArray = geoGroup.normalArray; for (i = 0; i < 4; i++) { colorArray[start + 3 * i] = C.r; colorArray[start + 3 * i + 1] = C.g; colorArray[start + 3 * i + 2] = C.b; } normalArray[start + 0] = -radius; normalArray[start + 1] = radius; normalArray[start + 2] = 0; normalArray[start + 3] = -radius; normalArray[start + 4] = -radius; normalArray[start + 5] = 0; normalArray[start + 6] = radius; normalArray[start + 7] = -radius; normalArray[start + 8] = 0; normalArray[start + 9] = radius; normalArray[start + 10] = radius; normalArray[start + 11] = 0; geoGroup.vertices += 4; //two faces var faceArray = geoGroup.faceArray; var faceoffset = geoGroup.faceidx; //not number faces, but index faceArray[faceoffset + 0] = startv; faceArray[faceoffset + 1] = startv + 1; faceArray[faceoffset + 2] = startv + 2; faceArray[faceoffset + 3] = startv + 2; faceArray[faceoffset + 4] = startv + 3; faceArray[faceoffset + 5] = startv; geoGroup.faceidx += 6; }; //dkoes - code for sphere imposters private drawAtomImposter(atom: AtomSpec, geo: Geometry) { if (!atom.style.sphere) return; var style = atom.style.sphere; if (style.hidden) return; var radius = this.getRadiusFromStyle(atom, style); var C = getColorFromStyle(atom, style); if ((atom.clickable === true || atom.hoverable) && (atom.intersectionShape !== undefined)) { var center = new Vector3(atom.x, atom.y, atom.z); atom.intersectionShape.sphere.push(new Sphere(center, radius)); } this.drawSphereImposter(geo, atom as XYZ, radius, C); }; private calculateDashes(from: XYZ, to: XYZ, radius: number, dashLength: number, gapLength: number) { // Calculate the length of a cylinder defined by two points 'from' and 'to'. var cylinderLength = Math.sqrt( Math.pow((from.x - to.x), 2) + Math.pow((from.y - to.y), 2) + Math.pow((from.z - to.z), 2) ); // Ensure non-negative values for radius, dashLength, and gapLength. // Adjust gapLength to include the radius of the cylinder. radius = Math.max(radius, 0); gapLength = Math.max(gapLength, 0) + 2 * radius; dashLength = Math.max(dashLength, 0.001); // Handle cases where the combined length of dash and gap exceeds the cylinder's length. // In such cases, use a single dash to represent the entire cylinder with no gaps. if (dashLength + gapLength > cylinderLength) { dashLength = cylinderLength; gapLength = 0; // No gap as the dash fills the entire cylinder. } // Calculate the total number of dash-gap segments that can fit within the cylinder. var totalSegments = Math.floor((cylinderLength - dashLength) / (dashLength + gapLength)) + 1; // Compute the total length covered by dashes. var totalDashLength = totalSegments * dashLength; // Recalculate gap length to evenly distribute remaining space among gaps. // This ensures dashes and gaps are evenly spaced within the cylinder. gapLength = (cylinderLength - totalDashLength) / totalSegments; var new_to; var new_from = new Vector3(from.x, from.y, from.z); var gapVector = new Vector3((to.x - from.x) / (cylinderLength / gapLength), (to.y - from.y) / (cylinderLength / gapLength), (to.z - from.z) / (cylinderLength / gapLength)); var dashVector = new Vector3((to.x - from.x) / (cylinderLength / dashLength), (to.y - from.y) / (cylinderLength / dashLength), (to.z - from.z) / (cylinderLength / dashLength)); var segments = []; for (var place = 0; place < totalSegments; place++) { new_to = new Vector3(new_from.x + dashVector.x, new_from.y + dashVector.y, new_from.z + dashVector.z); segments.push({ from: new_from, to: new_to }); new_from = new Vector3(new_to.x + gapVector.x, new_to.y + gapVector.y, new_to.z + gapVector.z); } return segments; } static drawStickImposter(geo: Geometry, from: XYZ, to: XYZ, radius: number, color: Color, fromCap:CAP = 0, toCap:CAP = 0) { //we need the four corners - two have from coord, two have to coord, the normal //is the opposing point, from which we can get the normal and length //also need the radius var geoGroup = geo.updateGeoGroup(4); var startv = geoGroup.vertices; var start = startv * 3; var vertexArray = geoGroup.vertexArray; var colorArray = geoGroup.colorArray; var radiusArray = geoGroup.radiusArray; var normalArray = geoGroup.normalArray; //encode extra bits of information in the color var r = color.r; var g = color.g; var b = color.b; var negateColor = function (c) { //set sign bit var n = -c; if (n == 0) n = -0.0001; return n; }; /* for sticks, always draw caps, but we could in theory set caps in color */ //4 vertices, distinguish between p1 and p2 with neg blue var pos = start; for (var i = 0; i < 4; i++) { vertexArray[pos] = from.x; normalArray[pos] = to.x; colorArray[pos] = r; pos++; vertexArray[pos] = from.y; normalArray[pos] = to.y; colorArray[pos] = g; pos++; vertexArray[pos] = from.z; normalArray[pos] = to.z; if (i < 2) colorArray[pos] = b; else colorArray[pos] = negateColor(b); pos++; } geoGroup.vertices += 4; radiusArray[startv] = -radius; radiusArray[startv + 1] = radius; radiusArray[startv + 2] = -radius; radiusArray[startv + 3] = radius; //two faces var faceArray = geoGroup.faceArray; var faceoffset = geoGroup.faceidx; //not number faces, but index faceArray[faceoffset + 0] = startv; faceArray[faceoffset + 1] = startv + 1; faceArray[faceoffset + 2] = startv + 2; faceArray[faceoffset + 3] = startv + 2; faceArray[faceoffset + 4] = startv + 3; faceArray[faceoffset + 5] = startv; geoGroup.faceidx += 6; }; // draws cylinders and small spheres (at bond radius) private drawBondSticks(atom: AtomSpec, atoms: AtomSpec[], geo: Geometry) { if (!atom.style.stick) return; var style = atom.style.stick; if (style.hidden) return; var atomBondR = style.radius || this.defaultStickRadius; var doubleBondScale = style.doubleBondScaling || 0.4; var tripleBondScale = style.tripleBondScaling || 0.25; var bondDashLength = style.dashedBondConfig?.dashLength || 0.1; var bondGapLength = style.dashedBondConfig?.gapLength || 0.25; var bondR = atomBondR; var atomSingleBond = style.singleBonds || false; var atomDashedBonds = style.dashedBonds || false; var fromCap = 0, toCap = 0; var atomneedsi, atom2needsi, i, singleBond, bstyle; var cylinder1a, cylinder1b, cylinder1c, cylinder2a, cylinder2b, cylinder2c; var C1 = getColorFromStyle(atom, style); var mp, mp2, mp3; if (!atom.capDrawn && atom.bonds.length < 4) fromCap = 2; var selectCylDrawMethod = (bondOrder) => { var drawMethod = geo.imposter ? GLModel.drawStickImposter : GLDraw.drawCylinder; if (!atomDashedBonds && bondOrder >= 1) { return drawMethod; } // draw dashes return (geo, from, to, radius, color, fromCap = 0, toCap = 0, dashLength = 0.1, gapLength = 0.25) => { var segments = this.calculateDashes(from, to, radius, dashLength, gapLength); segments.forEach(segment => { drawMethod(geo, segment.from, segment.to, radius, color, fromCap, toCap); }); }; }; for (i = 0; i < atom.bonds.length; i++) { var drawCyl = selectCylDrawMethod(atom.bondOrder[i]); var j = atom.bonds[i]; // our neighbor var atom2 = atoms[j]; //parsePDB, etc should only add defined bonds mp = mp2 = mp3 = null; if (atom.index < atom2.index) {// only draw if less, this // lets us combine // cylinders of the same // color var style2 = atom2.style; if (!style2.stick || style2.stick.hidden) continue; // don't sweat the details var C2 = getColorFromStyle(atom2, style2.stick); //support bond specific styles bondR = atomBondR; singleBond = atomSingleBond; if (atom.bondStyles && atom.bondStyles[i]) { bstyle = atom.bondStyles[i]; if (bstyle.iswire) { continue; } if (bstyle.radius) bondR = bstyle.radius; if (bstyle.singleBond) singleBond = true; if (typeof (bstyle.color1) != "undefined") { C1 = CC.color(bstyle.color1) as Color; } if (typeof (bstyle.color2) != "undefined") { C2 = CC.color(bstyle.color2) as Color; } } var p1 = new Vector3(atom.x, atom.y, atom.z); var p2 = new Vector3(atom2.x, atom2.y, atom2.z); // draw cylinders if (atom.bondOrder[i] <= 1 || singleBond || atom.bondOrder[i] > 3) { //TODO: aromatics at 4 if(atom.bondOrder[i] < 1) bondR *= atom.bondOrder[i]; if (!atom2.capDrawn && atom2.bonds.length < 4) toCap = 2; if (C1 != C2) { mp = new Vector3().addVectors(p1, p2) .multiplyScalar(0.5); drawCyl(geo, p1, mp, bondR, C1, fromCap, 0, bondDashLength, bondGapLength); drawCyl(geo, mp, p2, bondR, C2, 0, toCap, bondDashLength, bondGapLength); } else { drawCyl(geo, p1, p2, bondR, C1, fromCap, toCap, bondDashLength, bondGapLength); } atomneedsi = atom.clickable || atom.hoverable; atom2needsi = atom2.clickable || atom2.hoverable; if (atomneedsi || atom2needsi) { if (!mp) mp = new Vector3().addVectors(p1, p2).multiplyScalar(0.5); if (atomneedsi) { var cylinder1 = new Cylinder(p1, mp, bondR); var sphere1 = new Sphere(p1, bondR); atom.intersectionShape.cylinder.push(cylinder1); atom.intersectionShape.sphere.push(sphere1); } if (atom2needsi) { var cylinder2 = new Cylinder(p2, mp, bondR); var sphere2 = new Sphere(p2, bondR); atom2.intersectionShape.cylinder.push(cylinder2); atom2.intersectionShape.sphere.push(sphere2); } } } else if (atom.bondOrder[i] > 1) { //multi bond caps var mfromCap = 0; var mtoCap = 0; if (bondR != atomBondR) { //assume jmol style multiple bonds - the radius doesn't fit within atom sphere mfromCap = 2; mtoCap = 2; } var dir = p2.clone(); var v = null; dir.sub(p1); var r, p1a, p1b, p2a, p2b; v = this.getSideBondV(atom, atom2, i); if (atom.bondOrder[i] == 2) { r = bondR * doubleBondScale; v.multiplyScalar(r * 1.5); p1a = p1.clone(); p1a.add(v); p1b = p1.clone(); p1b.sub(v); p2a = p1a.clone(); p2a.add(dir); p2b = p1b.clone(); p2b.add(dir); if (C1 != C2) { mp = new Vector3().addVectors(p1a, p2a) .multiplyScalar(0.5); mp2 = new Vector3().addVectors(p1b, p2b) .multiplyScalar(0.5); drawCyl(geo, p1a, mp, r, C1, mfromCap, 0); drawCyl(geo, mp, p2a, r, C2, 0, mtoCap); drawCyl(geo, p1b, mp2, r, C1, mfromCap, 0); drawCyl(geo, mp2, p2b, r, C2, 0, mtoCap); } else { drawCyl(geo, p1a, p2a, r, C1, mfromCap, mtoCap); drawCyl(geo, p1b, p2b, r, C1, mfromCap, mtoCap); } atomneedsi = atom.clickable || atom.hoverable; atom2needsi = atom2.clickable || atom2.hoverable; if (atomneedsi || atom2needsi) { if (!mp) mp = new Vector3().addVectors(p1a, p2a) .multiplyScalar(0.5); if (!mp2) mp2 = new Vector3().addVectors(p1b, p2b) .multiplyScalar(0.5); if (atomneedsi) { cylinder1a = new Cylinder(p1a, mp, r); cylinder1b = new Cylinder(p1b, mp2, r); atom.intersectionShape.cylinder.push(cylinder1a); atom.intersectionShape.cylinder.push(cylinder1b); } if (atom2needsi) { cylinder2a = new Cylinder(p2a, mp, r); cylinder2b = new Cylinder(p2b, mp2, r); atom2.intersectionShape.cylinder.push(cylinder2a); atom2.intersectionShape.cylinder.push(cylinder2b); } } } else if (atom.bondOrder[i] == 3) { r = bondR * tripleBondScale; v.cross(dir); v.normalize(); v.multiplyScalar(r * 3); p1a = p1.clone(); p1a.add(v); p1b = p1.clone(); p1b.sub(v); p2a = p1a.clone(); p2a.add(dir); p2b = p1b.clone(); p2b.add(dir); if (C1 != C2) { mp = new Vector3().addVectors(p1a, p2a) .multiplyScalar(0.5); mp2 = new Vector3().addVectors(p1b, p2b) .multiplyScalar(0.5); mp3 = new Vector3().addVectors(p1, p2) .multiplyScalar(0.5); drawCyl(geo, p1a, mp, r, C1, mfromCap, 0); drawCyl(geo, mp, p2a, r, C2, 0, mtoCap); drawCyl(geo, p1, mp3, r, C1, fromCap, 0); drawCyl(geo, mp3, p2, r, C2, 0, toCap); drawCyl(geo, p1b, mp2, r, C1, mfromCap, 0); drawCyl(geo, mp2, p2b, r, C2, 0, mtoCap); } else { drawCyl(geo, p1a, p2a, r, C1, mfromCap, mtoCap); drawCyl(geo, p1, p2, r, C1, fromCap, toCap); drawCyl(geo, p1b, p2b, r, C1, mfromCap, mtoCap); } atomneedsi = atom.clickable || atom.hoverable; atom2needsi = atom2.clickable || atom2.hoverable; if (atomneedsi || atom2needsi) { if (!mp) mp = new Vector3().addVectors(p1a, p2a) .multiplyScalar(0.5); if (!mp2) mp2 = new Vector3().addVectors(p1b, p2b) .multiplyScalar(0.5); if (!mp3) mp3 = new Vector3().addVectors(p1, p2) .multiplyScalar(0.5); if (atomneedsi) { cylinder1a = new Cylinder(p1a.clone(), mp.clone(), r); cylinder1b = new Cylinder(p1b.clone(), mp2.clone(), r); cylinder1c = new Cylinder(p1.clone(), mp3.clone(), r); atom.intersectionShape.cylinder.push(cylinder1a); atom.intersectionShape.cylinder.push(cylinder1b); atom.intersectionShape.cylinder.push(cylinder1c); } if (atom2needsi) { cylinder2a = new Cylinder(p2a.clone(), mp.clone(), r); cylinder2b = new Cylinder(p2b.clone(), mp2.clone(), r); cylinder2c = new Cylinder(p2.clone(), mp3.clone(), r); atom2.intersectionShape.cylinder.push(cylinder2a); atom2.intersectionShape.cylinder.push(cylinder2b); atom2.intersectionShape.cylinder.push(cylinder2c); } } } } } } // draw non bonded heteroatoms as spheres var drawSphere = false; var numsinglebonds = 0; var differentradii = false; //also, if any bonds were drawn as multiples, need sphere for (i = 0; i < atom.bonds.length; i++) { singleBond = atomSingleBond; if (atom.bondStyles && atom.bondStyles[i]) { bstyle = atom.bondStyles[i]; if (bstyle.singleBond) singleBond = true; if (bstyle.radius && bstyle.radius != atomBondR) { differentradii = true; } } if (singleBond || atom.bondOrder[i] == 1) { numsinglebonds++; } } if (differentradii) { //jmol style double/triple bonds - no sphere if (numsinglebonds > 0) drawSphere = true; //unless needed as a cap } else if (numsinglebonds == 0 && (atom.bonds.length > 0 || style.showNonBonded)) { drawSphere = true; } if (drawSphere) { bondR = atomBondR; //do not use bond style as this can be variable, particularly //with jmol export of double/triple bonds if (geo.imposter) { this.drawSphereImposter(geo.sphereGeometry, atom as XYZ, bondR, C1); } else { GLDraw.drawSphere(geo, atom, bondR, C1); } } }; // go through all the atoms and regenerate their geometries // we try to have one geometry for each style since this is much much // faster // at some point we should optimize this to avoid unnecessary // recalculation /** param {AtomSpec[]} atoms */ private createMolObj(atoms:AtomSpec[], options?) { options = options || {}; var ret = new Object3D(); var cartoonAtoms = []; var lineGeometries: Record = {}; var crossGeometries:Record = {}; var drawSphereFunc = this.drawAtomSphere; var sphereGeometry: Geometry = null; var stickGeometry: Geometry = null; if (options.supportsImposters) { drawSphereFunc = this.drawAtomImposter; sphereGeometry = new Geometry(true); sphereGeometry.imposter = true; stickGeometry = new Geometry(true, true); stickGeometry.imposter = true; stickGeometry.sphereGeometry = new Geometry(true); //for caps stickGeometry.sphereGeometry.imposter = true; stickGeometry.drawnCaps = {}; } else if (options.supportsAIA) { drawSphereFunc = this.drawAtomInstanced; sphereGeometry = new Geometry(false, true, true); sphereGeometry.instanced = true; stickGeometry = new Geometry(true); //don't actually have instanced sticks } else { sphereGeometry = new Geometry(true); stickGeometry = new Geometry(true); } var i, j, n, testOpacities; var opacities: any = {}; var range = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; for (i = 0, n = atoms.length; i < n; i++) { var atom = atoms[i]; // recreate gl info for each atom as necessary // set up appropriate intersection spheres for clickable atoms if (atom && atom.style) { if ((atom.clickable || atom.hoverable) && atom.intersectionShape === undefined) atom.intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; testOpacities = { line: undefined, cross: undefined, stick: undefined, sphere: undefined }; for (j in testOpacities) { if (atom.style[j]) { if (atom.style[j].opacity) testOpacities[j] = parseFloat(atom.style[j].opacity); else testOpacities[j] = 1; } else testOpacities[j] = undefined; if (opacities[j]) { if (testOpacities[j] != undefined && opacities[j] != testOpacities[j]) { console.log("Warning: " + j + " opacity is ambiguous"); opacities[j] = 1; } } else opacities[j] = testOpacities[j]; } drawSphereFunc.call(this, atom, sphereGeometry); this.drawAtomClickSphere(atom); this.drawAtomCross(atom, crossGeometries); this.drawBondLines(atom, atoms, lineGeometries); this.drawBondSticks(atom, atoms, stickGeometry); if (typeof (atom.style.cartoon) !== "undefined" && !atom.style.cartoon.hidden) { //gradient color scheme range if (atom.style.cartoon.color === "spectrum" && typeof (atom.resi) === "number" && !atom.hetflag) { if (atom.resi < range[0]) range[0] = atom.resi; if (atom.resi > range[1]) range[1] = atom.resi; } cartoonAtoms.push(atom); } } } // create cartoon if needed - this is a whole model analysis if (cartoonAtoms.length > 0) { drawCartoon(ret, cartoonAtoms, range, this.defaultCartoonQuality); } // add sphere geometry if (sphereGeometry && sphereGeometry.vertices > 0) { //Initialize buffers in geometry sphereGeometry.initTypedArrays(); var sphereMaterial = null; var sphere = null; //create appropriate material if (sphereGeometry.imposter) { sphereMaterial = new SphereImposterMaterial({ ambient: 0x000000, vertexColors: true, reflectivity: 0 }); } else if (sphereGeometry.instanced) { sphere = new Geometry(true); GLDraw.drawSphere(sphere, { x: 0, y: 0, z: 0 }, 1, new Color(0.5, 0.5, 0.5)); sphere.initTypedArrays(); sphereMaterial = new InstancedMaterial({ sphereMaterial: new MeshLambertMaterial({ ambient: 0x000000, vertexColors: true, reflectivity: 0, }), sphere: sphere }); } else { //regular mesh sphereMaterial = new MeshLambertMaterial({ ambient: 0x000000, vertexColors: true, reflectivity: 0, }); } if (opacities.sphere < 1 && opacities.sphere >= 0) { sphereMaterial.transparent = true; sphereMaterial.opacity = opacities.sphere; } sphere = new Mesh(sphereGeometry, sphereMaterial); ret.add(sphere); } // add stick geometry if (stickGeometry.vertices > 0) { var stickMaterial = null; var ballMaterial = null; var balls = stickGeometry.sphereGeometry; if (!balls || typeof (balls.vertices) === 'undefined' || balls.vertices == 0) balls = null; //no balls //Initialize buffers in geometry stickGeometry.initTypedArrays(); if (balls) balls.initTypedArrays(); //create material var matvals = { ambient: 0x000000, vertexColors: true, reflectivity: 0 }; if (stickGeometry.imposter) { stickMaterial = new StickImposterMaterial(matvals); ballMaterial = new SphereImposterMaterial(matvals); } else { stickMaterial = new MeshLambertMaterial(matvals); ballMaterial = new MeshLambertMaterial(matvals); if (stickMaterial.wireframe) { stickGeometry.setUpWireframe(); if (balls) balls.setUpWireframe(); } } if (opacities.stick < 1 && opacities.stick >= 0) { stickMaterial.transparent = true; stickMaterial.opacity = opacities.stick; ballMaterial.transparent = true; ballMaterial.opacity = opacities.stick; } var sticks = new Mesh(stickGeometry, stickMaterial); ret.add(sticks); if (balls) { var stickspheres = new Mesh(balls, ballMaterial); ret.add(stickspheres); } } //var linewidth; // add any line geometries, distinguished by line width var linewidth; for (i in lineGeometries) { if (lineGeometries.hasOwnProperty(i)) { linewidth = i; var lineMaterial = new LineBasicMaterial({ linewidth: linewidth, vertexColors: true }); if (opacities.line < 1 && opacities.line >= 0) { lineMaterial.transparent = true; lineMaterial.opacity = opacities.line; } lineGeometries[i].initTypedArrays(); var line = new Line(lineGeometries[i], lineMaterial as Material, LineStyle.LinePieces); ret.add(line); } } // add any cross geometries for (i in crossGeometries) { if (crossGeometries.hasOwnProperty(i)) { linewidth = i; var crossMaterial = new LineBasicMaterial({ linewidth: linewidth, vertexColors: true }); if (opacities.cross < 1 && opacities.cross >= 0) { crossMaterial.transparent = true; crossMaterial.opacity = opacities.cross; } crossGeometries[i].initTypedArrays(); var cross = new Line(crossGeometries[i], crossMaterial as Material, LineStyle.LinePieces); ret.add(cross); } } //for BIOMT assembly if (this.dontDuplicateAtoms && this.modelData.symmetries && this.modelData.symmetries.length > 0) { var finalRet = new Object3D(); var t; for (t = 0; t < this.modelData.symmetries.length; t++) { var transformedRet = new Object3D(); transformedRet = ret.clone(); transformedRet.matrix.copy(this.modelData.symmetries[t]); transformedRet.matrixAutoUpdate = false; finalRet.add(transformedRet); } return finalRet; } return ret; }; /** * Return object representing internal state of * the model appropriate for passing to setInternalState * */ public getInternalState() { return { 'atoms': this.atoms, 'frames': this.frames }; }; /** * Overwrite the internal model state with the passed state. * */ public setInternalState(state) { this.atoms = state.atoms; this.frames = state.frames; this.molObj = null; }; /** * Returns crystallographic information if present. * * */ public getCrystData() { if (this.modelData.cryst) { // add the matrix if it is missing if (!this.modelData.cryst.matrix) { const cryst = this.modelData.cryst; this.modelData.cryst.matrix = conversionMatrix3( cryst.a, cryst.b, cryst.c, cryst.alpha, cryst.beta, cryst.gamma ); } return this.modelData.cryst; } else { return null; } }; /** * Set crystallographic information using three angles and three lengths * * @param {number} a - length of unit cell side * @param {number} b - length of unit cell side * @param {number} c - length of unit cell side * @param {number} alpha - unit cell angle in degrees (default 90) * @param {number} beta - unit cell angle in degrees (default 90) * @param {number} gamma - unit cell angle in degrees (default 90) */ public setCrystData(a?: number, b?:number, c?:number, alpha?:number, beta?:number, gamma?:number) { //I am assuming these a = a || 1.0; b = b || 1.0; c = c || 1.0; alpha = alpha || 90; beta = beta || 90; gamma = gamma || 90; const matrix = conversionMatrix3(a, b, c, alpha, beta, gamma); this.modelData.cryst = { 'a': a, 'b': b, 'c': c, 'alpha': alpha, 'beta': beta, 'gamma': gamma, 'matrix': matrix }; }; /** * Set the crystallographic matrix to the given matrix. * * This function removes `a`, `b`, `c`, `alpha`, `beta`, `gamma` from * the crystal data. * * @param {Matrix3} matrix - unit cell matrix */ public setCrystMatrix(matrix: Matrix3) { matrix = matrix || new Matrix3( 1, 0, 0, 0, 1, 0, 0, 0, 1 ); this.modelData.cryst = { 'matrix': matrix }; }; /** * Returns list of rotational/translational matrices if there is BIOMT data * Otherwise returns a list of just the ID matrix * * @return {Array} * */ public getSymmetries() { if (typeof (this.modelData.symmetries) == 'undefined') { this.modelData.symmetries = [this.idMatrix]; } return this.modelData.symmetries; }; /** * Sets symmetries based on specified matrices in list * * @param {Array} list * */ public setSymmetries(list) { if (typeof (list) == "undefined") { //delete sym data this.modelData.symmetries = [this.idMatrix]; } else { this.modelData.symmetries = list; } }; /** * Returns model id number * * @return {number} Model ID */ public getID() { return this.id; }; /** * Returns model's frames property, a list of atom lists * * @return {number} */ public getNumFrames() { return (this.frames.numFrames != undefined) ? this.frames.numFrames : this.frames.length; }; private adjustCoord(x1:number, x2:number, margin:number, adjust:number) { //return new value of x2 that isn't more than margin away var dist = x2 - x1; if (dist < -margin) { return x2 + adjust; } else if (dist > margin) { return x2 - adjust; } return x2; }; //go over current atoms in depth first order and ensure that connected //attoms aren't split across the box private adjustCoordinatesToBox() { if (!this.box) return; if (!this.atomdfs) return; var bx = this.box[0]; var by = this.box[1]; var bz = this.box[2]; var mx = bx * 0.9; var my = by * 0.9; var mz = bz * 0.9; for (var c = 0; c < this.atomdfs.length; c++) { //for each connected component var component = this.atomdfs[c]; for (var i = 1; i < component.length; i++) { //compare each atom to its previous and prevent coordinates from wrapping var atom = this.atoms[component[i][0]]; var prev = this.atoms[component[i][1]]; atom.x = this.adjustCoord(prev.x, atom.x, mx, bx); atom.y = this.adjustCoord(prev.y, atom.y, my, by); atom.z = this.adjustCoord(prev.z, atom.z, mz, bz); } } }; /** * Sets model's atomlist to specified frame * Sets to last frame if framenum out of range * * @param {number} framenum - model's atoms are set to this index in frames list * @return {Promise} */ public setFrame(framenum: number, viewer?: GLViewer) { //viewer only passed internally for unit cell var numFrames = this.getNumFrames(); let model = this; return new Promise(function (resolve, reject) { if (numFrames == 0) { //return; resolve(); } if (framenum < 0 || framenum >= numFrames) { framenum = numFrames - 1; } if (model.frames.url != undefined) { var url = model.frames.url; getbin(url + "/traj/frame/" + framenum + "/" + model.frames.path, undefined, 'POST', undefined).then(function (buffer) { var values = new Float32Array(buffer, 44); var count = 0; for (var i = 0; i < model.atoms.length; i++) { model.atoms[i].x = values[count++]; model.atoms[i].y = values[count++]; model.atoms[i].z = values[count++]; } //if a box was provided, check to see if we need to wrap connected components if (model.box && model.atomdfs) { model.adjustCoordinatesToBox(); } resolve(); }).catch(reject); } else { model.atoms = model.frames[framenum]; resolve(); } model.molObj = null; if (model.modelDatas && framenum < model.modelDatas.length) { model.modelData = model.modelDatas[framenum]; if (model.unitCellObjects && viewer) { viewer.removeUnitCell(model); viewer.addUnitCell(model); } } }); }; /** * Add atoms as frames of model * * @param {AtomSpec[]} atoms - atoms to be added */ public addFrame(atoms: AtomSpec[]) { this.frames.push(atoms); }; /** * If model atoms have dx, dy, dz properties (in some xyz files), vibrate populates the model's frame property based on parameters. * Model can then be animated * * @param {number} numFrames - number of frames to be created, default to 10 * @param {number} amplitude - amplitude of distortion, default to 1 (full) * @param {boolean} bothWays - if true, extend both in positive and negative directions by numFrames * @param {GLViewer} viewer - required if arrowSpec is provided * @param {ArrowSpec} arrowSpec - specification for drawing animated arrows. If color isn't specified, atom color (sphere, stick, line preference) is used. *@example $3Dmol.download("pdb:4UAA",viewer,{},function(){ viewer.setStyle({},{stick:{}}); viewer.vibrate(10, 1); viewer.animate({loop: "forward",reps: 1}); viewer.zoomTo(); viewer.render(); }); */ public vibrate(numFrames:number=10, amplitude:number=1, bothWays:boolean=false, viewer?:GLViewer, arrowSpec?:ArrowSpec) { var start = 0; var end = numFrames; if (bothWays) { start = -numFrames; end = numFrames; } //to enable multiple setting of vibrate with bothWays, must record original position if (this.frames !== undefined && this.frames.origIndex !== undefined) { this.setFrame(this.frames.origIndex); } else { this.setFrame(0); } if (start < end) this.frames = []; //clear if (bothWays) this.frames.origIndex = numFrames; for (var i = start; i < end; i++) { var newAtoms = []; var currframe = this.frames.length; if (i == 0 && !arrowSpec) { //still need to calculate if drawing arrows this.frames.push(this.atoms); continue; } for (var j = 0; j < this.atoms.length; j++) { var dx = getAtomProperty(this.atoms[j], 'dx'); var dy = getAtomProperty(this.atoms[j], 'dy'); var dz = getAtomProperty(this.atoms[j], 'dz'); var newVector = new Vector3(dx, dy, dz); var starting = new Vector3(this.atoms[j].x, this.atoms[j].y, this.atoms[j].z); var mult = (i * amplitude) / numFrames; newVector.multiplyScalar(mult); starting.add(newVector); var newAtom: any = {}; for (var k in this.atoms[j]) { newAtom[k] = this.atoms[j][k]; } newAtom.x = starting.x; newAtom.y = starting.y; newAtom.z = starting.z; newAtoms.push(newAtom); if (viewer && arrowSpec) { var spec = extend({}, arrowSpec); var arrowend = new Vector3(dx, dy, dz); arrowend.multiplyScalar(amplitude); arrowend.add(starting); spec.start = starting; spec.end = arrowend; spec.frame = currframe; if (!spec.color) { var s = newAtom.style.sphere; if (!s) s = newAtom.style.stick; if (!s) s = newAtom.style.line; spec.color = getColorFromStyle(newAtom, s); } viewer.addArrow(spec); } } this.frames.push(newAtoms); } }; // set default style and colors for atoms public setAtomDefaults(atoms: AtomSpec[]) { for (let i = 0; i < atoms.length; i++) { let atom = atoms[i]; if (atom) { atom.style = atom.style || deepCopy(GLModel.defaultAtomStyle); atom.color = atom.color || this.ElementColors[atom.elem] || this.defaultColor; atom.model = this.id; if (atom.clickable || atom.hoverable) atom.intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; } } }; /** add atoms to this model from molecular data string * * @param {string|ArrayBuffer} data - atom structure file input data string, for gzipped input use ArrayBuffer * @param {string} format - input file string format (e.g 'pdb', 'sdf', 'sdf.gz', etc.) * @param {ParserOptionsSpec} options - format dependent options. Attributes depend on the input format */ public addMolData(data: string|ArrayBuffer, format:string, options: ParserOptionsSpec={}) { var parsedAtoms = GLModel.parseMolData(data, format, options); this.dontDuplicateAtoms = !options.duplicateAssemblyAtoms; var mData = parsedAtoms.modelData; if (mData) { if (Array.isArray(mData)) { this.modelData = mData[0]; if (options.frames) { this.modelDatas = mData; } } else { this.modelData = mData; } } if (parsedAtoms.box) { this.box = parsedAtoms.box; } else { this.box = null; } if (this.frames.length == 0) { //first call for (let i = 0; i < parsedAtoms.length; i++) { if (parsedAtoms[i].length != 0) this.frames.push(parsedAtoms[i]); } if (this.frames[0]) this.atoms = this.frames[0]; } else { //subsequent calls if (options.frames) { //add to new frame for (let i = 0; i < parsedAtoms.length; i++) { this.frames.push(parsedAtoms[i]); } } else { //add atoms to current frame for (var i = 0; i < parsedAtoms.length; i++) { this.addAtoms(parsedAtoms[i]); } } } for (let i = 0; i < this.frames.length; i++) { this.setAtomDefaults(this.frames[i]); } if (options.vibrate && options.vibrate.frames && options.vibrate.amplitude) { //fill in vibrational modes this.vibrate(options.vibrate.frames, options.vibrate.amplitude); } if (options.style) { this.setStyle({}, options.style); } }; public setDontDuplicateAtoms(dup:boolean) { this.dontDuplicateAtoms = dup; }; public setModelData(mData) { this.modelData = mData; }; //return true if atom value matches property val private propertyMatches(atomval, val) { if (atomval == val) { return true; } else if (typeof (val) == 'string' && typeof (atomval) == 'number') { //support numerical integer ranges, e.g. resi: 3-7 var match = val.match(/(-?\d+)\s*-\s*(-?\d+)/); if (match) { var lo = parseInt(match[1]); var hi = parseInt(match[2]); if (match && atomval >= lo && atomval <= hi) { return true; } } } return false; }; // make a deep copy of a selection object and create caches of expensive // selections. We create a copy so caches are not attached to user // supplied objects where the user might change them invalidating the cache. // This does not support arbitrary // javascript objects, but support enough for eveything that is // used in selections: number, string, boolean, functions; as well // as arrays and nested objects with values of the aformentioned // types. private static deepCopyAndCache(selobject, model) { if (typeof selobject != 'object' || selobject == null) return selobject; if (selobject.__cache_created) return selobject; //already done const copy: any = {}; for (const key in selobject) { const item = selobject[key]; if (Array.isArray(item)) { // handle array separatly from other typeof == "object" // elements copy[key] = []; for (let i = 0; i < item.length; i++) { copy[key].push(GLModel.deepCopyAndCache(item[i], model)); } } else if (typeof item === "object" && key != "properties" && key != "model") { copy[key] = GLModel.deepCopyAndCache(item, model); } else { copy[key] = item; } //create caches of expensive selection types - the cache //stores the atoms matching the selection type if (key == "and" || key == "or") { // create a list of sets of matching atoms indexes for // each sub-selection const results = []; for (const subSelection of copy[key]) { const set = new Set(); for (const match of model.selectedAtoms(subSelection)) { set.add(match.index); } results.push(set); } if (key == "and") { // get the intersection of two sets const intersect = function (first, other) { const result = new Set(); for (const elem of other) { if (first.has(elem)) { result.add(elem); } } return result; }; let intersection = new Set(results[0]); for (const set of results.splice(1)) { intersection = intersect(intersection, set); } copy[key].__cached_results = intersection; } else if (key == "or") { const union = new Set(); for (const set of results) { for (const elem of set) { union.add(elem); } } copy[key].__cached_results = union; } } } copy.__cache_created = true; return copy; }; private static readonly ignoredKeys = new Set(["props", "invert", "model", "frame", "byres", "expand", "within", "and", "or", "not"]); /** given a selection specification, return true if atom is selected. * Does not support context-aware selectors like expand/within/byres. * * @param {AtomSpec} atom * @param {AtomSelectionSpec} sel * @return {boolean} */ public atomIsSelected(atom:AtomSpec, sel?:AtomSelectionSpec) { if (typeof (sel) === "undefined") return true; // undef gets all var invert = !!sel.invert; var ret = true; for (var key in sel) { if (key == "and" || key == "or" || key == "not") { //boolean operators if (key == "not") { if (this.atomIsSelected(atom, sel[key])) { ret = false; break; } } else { //"or" and "and" // these selections are expensive so when called via //selectedAtoms shoudl be cached - but if atomIsSelected //is called directly create the cache if (sel[key].__cached_results === undefined) { sel = GLModel.deepCopyAndCache(sel, this); } ret = sel[key].__cached_results.has(atom.index); if (!ret) { break; } } } else if (key === 'predicate') { //a user supplied function for evaluating atoms if (!sel.predicate(atom)) { ret = false; break; } } else if (key == "properties" && atom[key]) { for (var propkey in sel.properties) { if (propkey.startsWith("__cache")) continue; if (typeof (atom.properties[propkey]) === 'undefined') { ret = false; break; } if (atom.properties[propkey] != sel.properties[propkey]) { ret = false; break; } } } else if (sel.hasOwnProperty(key) && !GLModel.ignoredKeys.has(key) && !key.startsWith('__cache')) { // if something is in sel, atom must have it if (typeof (atom[key]) === "undefined") { ret = false; break; } var isokay = false; if (key === "bonds") { //special case counting number of bonds, for selecting nonbonded mostly var val = sel[key]; if (val != atom.bonds.length) { ret = false; break; } } else if (Array.isArray(sel[key])) { // can be any of the listed values var valarr = sel[key]; var atomval = atom[key]; for (let i = 0; i < valarr.length; i++) { if (this.propertyMatches(atomval, valarr[i])) { isokay = true; break; } } if (!isokay) { ret = false; break; } } else { // single match let val = sel[key]; if (!this.propertyMatches(atom[key], val)) { ret = false; break; } } } } return invert ? !ret : ret; }; private static squaredDistance(atom1:XYZ|AtomSpec, atom2:XYZ|AtomSpec) { var xd = atom2.x - atom1.x; var yd = atom2.y - atom1.y; var zd = atom2.z - atom1.z; return xd * xd + yd * yd + zd * zd; }; /** returns a list of atoms in the expanded bounding box, but not in the current one * * Bounding box: * * [ [ xmin, ymin, zmin ], * [ xmax, ymax, zmax ], * [ xctr, yctr, zctr ] ] * **/ private expandAtomList(atomList: AtomSpec[], amt:number) { if (amt <= 0) return atomList; var pb = getExtent(atomList, undefined); // previous bounding box var nb = [[], [], []]; // expanded bounding box for (var i = 0; i < 3; i++) { nb[0][i] = pb[0][i] - amt; nb[1][i] = pb[1][i] + amt; nb[2][i] = pb[2][i]; } // look in added box "shell" for new atoms var expand = []; for (let i = 0; i < this.atoms.length; i++) { var x = this.atoms[i].x; var y = this.atoms[i].y; var z = this.atoms[i].z; if (x >= nb[0][0] && x <= nb[1][0] && y >= nb[0][1] && y <= nb[1][1] && z >= nb[0][2] && z <= nb[1][2]) { if (!(x >= pb[0][0] && x <= pb[1][0] && y >= pb[0][1] && y <= pb[1][1] && z >= pb[0][2] && z <= pb[1][2])) { expand.push(this.atoms[i]); } } } return expand; }; private static getFloat(val: string|number): number { if(typeof(val) === 'number') return val; else return parseFloat(val); } /** return list of atoms selected by sel, this is specific to glmodel * * @param {AtomSelectionSpec} sel * @return {Object[]} * @example $3Dmol.download("pdb:4wwy",viewer,{},function(){ var atoms = viewer.selectedAtoms({chain:'A'}); for(var i = 0, n = atoms.length; i < n; i++) { atoms[i].b = 0.0; } viewer.setStyle({cartoon:{colorscheme:{prop:'b',gradient: 'roygb',min:0,max:30}}}); viewer.render(); }); */ public selectedAtoms(sel: AtomSelectionSpec, from?: AtomSpec[]): AtomSpec[] { var ret = []; // make a copy of the selection to allow caching results without // the possibility for the user to change the selection and this // code not noticing the changes sel = GLModel.deepCopyAndCache(sel || {}, this); if (!from) from = this.atoms; var aLength = from.length; for (var i = 0; i < aLength; i++) { var atom = from[i]; if (atom) { if (this.atomIsSelected(atom, sel)) ret.push(atom); } } // expand selection by some distance if (sel.hasOwnProperty("expand")) { // get atoms in expanded bounding box const exdist:number = GLModel.getFloat(sel.expand); let expand = this.expandAtomList(ret, exdist); let retlen = ret.length; const thresh = exdist * exdist; for (let i = 0; i < expand.length; i++) { for (let j = 0; j < retlen; j++) { var dist = GLModel.squaredDistance(expand[i], ret[j]); if (dist < thresh && dist > 0) { ret.push(expand[i]); } } } } // selection within distance of sub-selection if (sel.hasOwnProperty("within") && sel.within.hasOwnProperty("sel") && sel.within.hasOwnProperty("distance")) { // get atoms in second selection var sel2 = this.selectedAtoms(sel.within.sel, this.atoms); var within = {}; const dist = GLModel.getFloat(sel.within.distance); const thresh = dist * dist; for (let i = 0; i < sel2.length; i++) { for (let j = 0; j < ret.length; j++) { let dist = GLModel.squaredDistance(sel2[i], ret[j]); if (dist < thresh && dist > 0) { within[j] = 1; } } } var newret = []; if (sel.within.invert) { for (let j = 0; j < ret.length; j++) { if (!within[j]) newret.push(ret[j]); } } else { for (let j in within) { newret.push(ret[j]); } } ret = newret; } // byres selection flag if (sel.hasOwnProperty("byres")) { // Keep track of visited residues, visited atoms, and atom stack var vResis = {}; var vAtoms = []; var stack = []; for (let i = 0; i < ret.length; i++) { // Check if atom is part of a residue, and that the residue hasn't been traversed yet let atom = ret[i]; var c = atom.chain; var r = atom.resi; if (vResis[c] === undefined) vResis[c] = {}; if (atom.hasOwnProperty("resi") && vResis[c][r] === undefined) { // Perform a depth-first search of atoms with the same resi vResis[c][r] = true; stack.push(atom); while (stack.length > 0) { atom = stack.pop(); c = atom.chain; r = atom.resi; if (vAtoms[atom.index] === undefined) { vAtoms[atom.index] = true; for (var j = 0; j < atom.bonds.length; j++) { var atom2 = this.atoms[atom.bonds[j]]; if (vAtoms[atom2.index] === undefined && atom2.hasOwnProperty("resi") && atom2.chain == c && atom2.resi == r) { stack.push(atom2); ret.push(atom2); } } } } } } } return ret; }; /** Add list of new atoms to model. Adjusts bonds appropriately. * * @param {AtomSpec[]} newatoms * @example * var atoms = [{elem: 'C', x: 0, y: 0, z: 0, bonds: [1,2], bondOrder: [1,2]}, {elem: 'O', x: -1.5, y: 0, z: 0, bonds: [0]},{elem: 'O', x: 1.5, y: 0, z: 0, bonds: [0], bondOrder: [2]}]; viewer.setBackgroundColor(0xffffffff); var m = viewer.addModel(); m.addAtoms(atoms); m.setStyle({},{stick:{}}); viewer.zoomTo(); viewer.render(); */ public addAtoms(newatoms: AtomSpec[]) { this.molObj = null; var start = this.atoms.length; var indexmap = []; // mapping from old index to new index var i; for (i = 0; i < newatoms.length; i++) { if (typeof (newatoms[i].index) == "undefined") newatoms[i].index = i; if (typeof (newatoms[i].serial) == "undefined") newatoms[i].serial = i; indexmap[newatoms[i].index] = start + i; } // copy and push newatoms onto atoms for (i = 0; i < newatoms.length; i++) { var olda = newatoms[i]; var nindex = indexmap[olda.index]; var a = extend({}, olda); a.index = nindex; a.bonds = []; a.bondOrder = []; a.model = this.id; a.style = a.style || deepCopy(GLModel.defaultAtomStyle); if (typeof (a.color) == "undefined") a.color = this.ElementColors[a.elem] || this.defaultColor; // copy over all bonds contained in selection, // updating indices appropriately var nbonds = olda.bonds ? olda.bonds.length : 0; for (var j = 0; j < nbonds; j++) { var neigh = indexmap[olda.bonds[j]]; if (typeof (neigh) != "undefined") { a.bonds.push(neigh); a.bondOrder.push(olda.bondOrder ? olda.bondOrder[j] : 1); } } this.atoms.push(a); } }; /** Assign bonds based on atomic coordinates. * This currently uses a primitive distance-based algorithm that does not * consider valence constraints and will only create single bonds. */ public assignBonds() { assignBonds(this.atoms, {assignBonds: true}); } /** Remove specified atoms from model * * @param {AtomSpec[]} badatoms - list of atoms */ public removeAtoms(badatoms: AtomSpec[]) { this.molObj = null; // make map of all baddies var baddies = []; var i; for (i = 0; i < badatoms.length; i++) { baddies[badatoms[i].index] = true; } // create list of good atoms var newatoms = []; for (i = 0; i < this.atoms.length; i++) { var a = this.atoms[i]; if (!baddies[a.index]) newatoms.push(a); } // clear it all out this.atoms = []; // and add back in to get updated bonds this.addAtoms(newatoms); }; /** Set atom style of selected atoms * * @param {AtomSelectionSpec} sel * @param {AtomStyleSpec} style * @param {boolean} add - if true, add to current style, don't replace @example $3Dmol.download("pdb:4UB9",viewer,{},function(){ viewer.setBackgroundColor(0xffffffff); viewer.setStyle({chain:'A'},{line:{hidden:true,colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.Sinebow($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'B'},{line:{colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.Sinebow($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'C'},{cross:{hidden:true,colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.Sinebow($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'D'},{cross:{colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.RWB($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'E'},{cross:{radius:2.0,colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.RWB($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'F'},{stick:{hidden:true,colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.RWB($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'G'},{stick:{radius:0.8,colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.ROYGB($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.setStyle({chain:'H'},{stick:{singleBonds:true,colorscheme:{prop:'b',gradient: new $3Dmol.Gradient.ROYGB($3Dmol.getPropertyRange(viewer.selectedAtoms(),'b'))}}}); viewer.render(); }); */ public setStyle(sel:AtomSelectionSpec|AtomStyleSpec|string, style?:AtomStyleSpec|string, add?) { if (typeof (style) === 'undefined' && typeof (add) == 'undefined') { //if a single argument is provided, assume it is a style and select all style = sel as AtomStyleSpec|string; sel = {}; } sel = sel as AtomSelectionSpec; //if type is just a string, promote it to an object if (typeof (style) === 'string') { style = specStringToObject(style); } var changedAtoms = false; // somethings we only calculate if there is a change in a certain // style, although these checks will only catch cases where both // are either null or undefined var that = this; var setStyleHelper = function (atomArr) { var selected = that.selectedAtoms(sel as AtomSelectionSpec, atomArr); for (let i = 0; i < atomArr.length; i++) { if (atomArr[i]) atomArr[i].capDrawn = false; //reset for proper stick render } for (let i = 0; i < selected.length; i++) { changedAtoms = true; if (selected[i].clickable || selected[i].hoverable) selected[i].intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; if (!add) selected[i].style = {}; for (let s in style as AtomStyleSpec) { if (style.hasOwnProperty(s)) { selected[i].style[s] = selected[i].style[s] || {}; //create distinct object for each atom Object.assign(selected[i].style[s], style[s]); } } } }; if(sel.frame !== undefined && sel.frame < this.frames.length) { //set specific frame only let frame = sel.frame; if(frame < 0) frame = this.frames.length+frame; setStyleHelper(this.frames[frame]); } else { setStyleHelper(this.atoms); for (var i = 0; i < this.frames.length; i++) { if (this.frames[i] !== this.atoms) setStyleHelper(this.frames[i]); } } if (changedAtoms) this.molObj = null; //force rebuild }; /** Set clickable and callback of selected atoms * * @param {AtomSelectionSpec} sel - atom selection to apply clickable settings to * @param {boolean} clickable - whether click-handling is enabled for the selection * @param {function} callback - function called when an atom in the selection is clicked */ public setClickable(sel:AtomSelectionSpec, clickable:boolean, callback) { // make sure clickable is a boolean clickable = !!clickable; callback = makeFunction(callback); if (callback === null) { console.log("Callback is not a function"); return; } var selected = this.selectedAtoms(sel, this.atoms); var len = selected.length; for (let i = 0; i < len; i++) { selected[i].intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; selected[i].clickable = clickable; if (callback) selected[i].callback = callback; } if (len > 0) this.molObj = null; // force rebuild to get correct intersection shapes }; /** Set hoverable and callback of selected atoms * * @param {AtomSelectionSpec} sel - atom selection to apply hoverable settings to * @param {boolean} hoverable - whether hover-handling is enabled for the selection * @param {function} hover_callback - function called when an atom in the selection is hovered over * @param {function} unhover_callback - function called when the mouse moves out of the hover area */ public setHoverable(sel:AtomSelectionSpec, hoverable:boolean, hover_callback, unhover_callback) { // make sure hoverable is a boolean hoverable = !!hoverable; hover_callback = makeFunction(hover_callback); unhover_callback = makeFunction(unhover_callback); // report to console if hover_callback is not a valid function if (hover_callback === null) { console.log("Hover_callback is not a function"); return; } // report to console if unhover_callback is not a valid function if (unhover_callback === null) { console.log("Unhover_callback is not a function"); return; } var selected = this.selectedAtoms(sel, this.atoms); var len = selected.length; for (let i = 0; i < len; i++) { selected[i].intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; selected[i].hoverable = hoverable; if (hover_callback) selected[i].hover_callback = hover_callback; if (unhover_callback) selected[i].unhover_callback = unhover_callback; } if (len > 0) this.molObj = null; // force rebuild to get correct intersection shapes }; /** enable context menu of selected atoms * * @param {AtomSelectionSpec} sel - atom selection to apply hoverable settings to * @param {boolean} contextMenuEnabled - whether contextMenu-handling is enabled for the selection */ public enableContextMenu(sel:AtomSelectionSpec, contextMenuEnabled) { // make sure contextMenuEnabled is a boolean contextMenuEnabled = !!contextMenuEnabled; var i; var selected = this.selectedAtoms(sel, this.atoms); var len = selected.length; for (i = 0; i < len; i++) { selected[i].intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; selected[i].contextMenuEnabled = contextMenuEnabled; } if (len > 0) this.molObj = null; // force rebuild to get correct intersection shapes }; /** given a mapping from element to color, set atom colors * * @param {AtomSelectionSpec} sel * @param {object} colors */ public setColorByElement(sel: AtomSelectionSpec, colors) { if (this.molObj !== null && GLModel.sameObj(colors, this.lastColors)) return; // don't recompute this.lastColors = colors; var atoms = this.selectedAtoms(sel, atoms); if (atoms.length > 0) this.molObj = null; // force rebuild for (var i = 0; i < atoms.length; i++) { var a = atoms[i]; if (typeof (colors[a.elem]) !== "undefined") { a.color = colors[a.elem]; } } }; /** * @param {AtomSelectionSpec} sel * @param {string} prop * @param {Gradient|string} scheme */ public setColorByProperty(sel: AtomSelectionSpec, prop: string, scheme: Gradient|string, range?) { var i, a; var atoms = this.selectedAtoms(sel, atoms); this.lastColors = null; // don't bother memoizing if (atoms.length > 0) this.molObj = null; // force rebuild if (typeof scheme === 'string' && typeof (Gradient.builtinGradients[scheme]) != "undefined") { scheme = new Gradient.builtinGradients[scheme](); } scheme = scheme as Gradient; if (!range) { //no explicit range, get from scheme range = scheme.range(); } if (!range) { //no range in scheme, compute the range for this model range = getPropertyRange(atoms, prop); } // now apply colors using scheme for (i = 0; i < atoms.length; i++) { a = atoms[i]; var val = getAtomProperty(a, prop); if (val != null) { a.color = scheme.valueToHex(parseFloat(a.properties[prop]), range); } } }; /** * @deprecated use setStyle and colorfunc attribute * @param {AtomSelectionSpec} sel - selection object * @param {function} func - function to be used to set the color @example $3Dmol.download("pdb:4UAA",viewer,{},function(){ viewer.setBackgroundColor(0xffffffff); var colorAsSnake = function(atom) { return atom.resi % 2 ? 'white': 'green' }; viewer.setStyle( {}, { cartoon: {colorfunc: colorAsSnake }}); viewer.render(); }); */ public setColorByFunction(sel: AtomSelectionSpec, colorfun) { var atoms = this.selectedAtoms(sel, atoms); if (typeof (colorfun) !== 'function') return; this.lastColors = null; // don't bother memoizing if (atoms.length > 0) this.molObj = null; // force rebuild // now apply colorfun for (let i = 0; i < atoms.length; i++) { let a = atoms[i]; a.color = colorfun(a); } }; /** Convert the model into an object in the format of a ChemDoodle JSON model. * * @param {boolean} whether or not to include style information. Defaults to false. * @return {Object} */ public toCDObject(includeStyles: boolean = false) { var out: any = { a: [], b: [] }; if (includeStyles) { out.s = []; } for (let i = 0; i < this.atoms.length; i++) { let atomJSON: any = {}; let atom = this.atoms[i]; atomJSON.x = atom.x; atomJSON.y = atom.y; atomJSON.z = atom.z; if (atom.elem != "C") { atomJSON.l = atom.elem; } if (includeStyles) { var s = 0; while (s < out.s.length && (JSON.stringify(atom.style) !== JSON.stringify(out.s[s]))) { s++; } if (s === out.s.length) { out.s.push(atom.style); } if (s !== 0) { atomJSON.s = s; } } out.a.push(atomJSON); for (let b = 0; b < atom.bonds.length; b++) { let firstAtom = i; let secondAtom = atom.bonds[b]; if (firstAtom >= secondAtom) continue; let bond: any = { b: firstAtom, e: secondAtom }; let bondOrder = atom.bondOrder[b]; if (bondOrder != 1) { bond.o = bondOrder; } out.b.push(bond); } } return out; }; /** manage the globj for this model in the possed modelGroup - if it has to be regenerated, remove and add * * @param {Object3D} group * @param Object options */ public globj(group, options) { if (this.molObj === null || options.regen) { // have to regenerate this.molObj = this.createMolObj(this.atoms, options); if (this.renderedMolObj) { // previously rendered, remove group.remove(this.renderedMolObj); this.renderedMolObj = null; } this.renderedMolObj = this.molObj.clone(); if (this.hidden) { this.renderedMolObj.setVisible(false); this.molObj.setVisible(false); } group.add(this.renderedMolObj); } }; /** return a VRML string representation of the model. Does not include VRML header information * @return VRML */ public exportVRML() { //todo: export spheres and cylinder objects instead of all mesh var tmpobj = this.createMolObj(this.atoms, { supportsImposters: false, supportsAIA: false }); return tmpobj.vrml(); }; /** Remove any renderable mol object from scene * * @param {Object3D} group */ public removegl(group) { if (this.renderedMolObj) { //dispose of geos and materials if (this.renderedMolObj.geometry !== undefined) this.renderedMolObj.geometry.dispose(); if (this.renderedMolObj.material !== undefined) this.renderedMolObj.material.dispose(); group.remove(this.renderedMolObj); this.renderedMolObj = null; } this.molObj = null; }; /** * Don't show this model in future renderings. Keep all styles and state * so it can be efficiencly shown again. * * * @see GLModel#show * @example $3Dmol.download("pdb:3ucr",viewer,{},function(){ viewer.setStyle({},{stick:{}}); viewer.getModel().hide(); viewer.render(); }); */ public hide() { this.hidden = true; if (this.renderedMolObj) this.renderedMolObj.setVisible(false); if (this.molObj) this.molObj.setVisible(false); }; /** * Unhide a hidden model * @see GLModel#hide * @example $3Dmol.download("pdb:3ucr",viewer,{},function(){ viewer.setStyle({},{stick:{}}); viewer.getModel().hide(); viewer.render( ) viewer.getModel().show() viewer.render(); }); */ public show() { this.hidden = false; if (this.renderedMolObj) this.renderedMolObj.setVisible(true); if (this.molObj) this.molObj.setVisible(true); }; /** Create labels for atoms that show the value of the passed property. * * @param {String} prop - property name * @param {AtomSelectionSpec} sel * @param {GLViewer} viewer * @param {LabelSpec} options */ public addPropertyLabels(prop: string, sel: AtomSelectionSpec, viewer: GLViewer, style: LabelSpec) { var atoms = this.selectedAtoms(sel, atoms); var mystyle = deepCopy(style); for (var i = 0; i < atoms.length; i++) { var a = atoms[i]; var label = null; if (typeof (a[prop]) != 'undefined') { label = String(a[prop]); } else if (typeof (a.properties[prop]) != 'undefined') { label = String(a.properties[prop]); } if (label != null) { mystyle.position = a; viewer.addLabel(label, mystyle); } } }; /** Create labels for residues of selected atoms. * Will create a single label at the center of mass of all atoms * with the same chain,resn, and resi. * * @param {AtomSelectionSpec} sel * @param {GLViewer} viewer * @param {LabelSpec} options * @param {boolean} byframe - if true, create labels for every individual frame, not just current; frames must be loaded already */ public addResLabels(sel: AtomSelectionSpec, viewer: GLViewer, style: LabelSpec, byframe:boolean=false) { var created_labels = []; var helper = function (model, framenum?) { var atoms = model.selectedAtoms(sel, atoms); var bylabel = {}; //collect by chain:resn:resi for (var i = 0; i < atoms.length; i++) { var a = atoms[i]; var c = a.chain; var resn = a.resn; var resi = a.resi; var label = resn + '' + resi; if (!bylabel[c]) bylabel[c] = {}; if (!bylabel[c][label]) bylabel[c][label] = []; bylabel[c][label].push(a); } var mystyle = deepCopy(style); //now compute centers of mass for (let c in bylabel) { if (bylabel.hasOwnProperty(c)) { var labels = bylabel[c]; for (let label in labels) { if (labels.hasOwnProperty(label)) { let atoms = labels[label]; let sum = new Vector3(0, 0, 0); for (let i = 0; i < atoms.length; i++) { let a = atoms[i]; sum.x += a.x; sum.y += a.y; sum.z += a.z; } sum.divideScalar(atoms.length); mystyle.position = sum; mystyle.frame = framenum; let l = viewer.addLabel(label, mystyle, undefined, true); created_labels.push(l); } } } } }; if (byframe) { var n = this.getNumFrames(); let savedatoms = this.atoms; for (let i = 0; i < n; i++) { if (this.frames[i]) { this.atoms = this.frames[i]; helper(this, i); } } this.atoms = savedatoms; } else { helper(this); } return created_labels; }; //recurse over the current atoms to establish a depth first order private setupDFS() { this.atomdfs = []; var self = this; var visited = new Int8Array(this.atoms.length); visited.fill(0); var search = function (i, prev, component) { //add i to component and recursive explore connected atoms component.push([i, prev]); var atom = self.atoms[i]; visited[i] = 1; for (var b = 0; b < atom.bonds.length; b++) { var nexti = atom.bonds[b]; if (self.atoms[nexti] && !visited[nexti]) { search(nexti, i, component); } } }; for (var i = 0; i < this.atoms.length; i++) { var atom = this.atoms[i]; if (atom && !visited[i]) { var component = []; search(i, -1, component); this.atomdfs.push(component); } } }; /** * Set coordinates from remote trajectory file. * @param {string} url - contains the url where mdsrv has been hosted * @param {string} path - contains the path of the file (/filename) * @return {Promise} */ public setCoordinatesFromURL(url:string, path:string) { this.frames = []; var self = this; if (this.box) this.setupDFS(); if (!url.startsWith('http')) url = 'http://' + url; return get(url + "/traj/numframes/" + path, function (numFrames) { if (!isNaN(parseInt(numFrames))) { self.frames.push(self.atoms); self.frames.numFrames = numFrames; self.frames.url = url; self.frames.path = path; return self.setFrame(0); } }); }; /** * Set coordinates for the atoms from provided trajectory file. * @param {string|ArrayBuffer} str - contains the data of the file * @param {string} format - contains the format of the file (mdcrd, inpcrd, pdb, netcdf, or array). Arrays should be TxNx3 where T is the number of timesteps and N the number of atoms. @example let m = viewer.addModel() //create an empty model m.addAtoms([{x:0,y:0,z:0,elem:'C'},{x:2,y:0,z:0,elem:'C'}]) //provide a list of dictionaries representing the atoms viewer.setStyle({'sphere':{}}) m.setCoordinates([[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [2.8888888359069824, 0.0, 0.0]], [[0.0, 0.0, 0.0], [3.777777671813965, 0.0, 0.0]], [[0.0, 0.0, 0.0], [4.666666507720947, 0.0, 0.0]], [[0.0, 0.0, 0.0], [5.55555534362793, 0.0, 0.0]], [[0.0, 0.0, 0.0], [6.44444465637207, 0.0, 0.0]], [[0.0, 0.0, 0.0], [7.333333492279053, 0.0, 0.0]], [[0.0, 0.0, 0.0], [8.222222328186035, 0.0, 0.0]], [[0.0, 0.0, 0.0], [9.11111068725586, 0.0, 0.0]], [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]],'array'); viewer.animate({loop: "forward",reps: 1}); viewer.zoomTo(); viewer.zoom(0.5); viewer.render(); */ public setCoordinates(str: string | ArrayBuffer, format:string) { format = format || ""; if (!str) return []; // leave an empty model if (/\.gz$/.test(format)) { // unzip gzipped files format = format.replace(/\.gz$/, ''); try { str = inflateString(str) } catch (err) { console.log(err); } } var supportedFormats = { "mdcrd": "", "inpcrd": "", "pdb": "", "netcdf": "", "array": "" }; if (supportedFormats.hasOwnProperty(format)) { this.frames = []; var atomCount = this.atoms.length; var values = GLModel.parseCrd(str, format); var count = 0; while (count < values.length) { var temp = []; for (var i = 0; i < atomCount; i++) { var newAtom = {}; for (var k in this.atoms[i]) { newAtom[k] = this.atoms[i][k]; } temp[i] = newAtom; temp[i].x = values[count++]; temp[i].y = values[count++]; temp[i].z = values[count++]; } this.frames.push(temp); } this.atoms = this.frames[0]; return this.frames; } return []; }; /** * add atomSpecs to validAtomSelectionSpecs * @deprecated * @param {Array} customAtomSpecs - array of strings that can be used as atomSelectionSpecs * this is to prevent the 'Unknown Selector x' message on the console for the strings passed. * These messages are no longer generated as, in theory, typescript will catch problems at compile time. * In practice, there may still be issues at run-time but we don't check for them... * * What we should do is use something like https://github.com/woutervh-/typescript-is to do runtime * type checking, but it currently doesn't work with our types... */ public addAtomSpecs(customAtomSpecs) { }; static parseCrd(data, format:string) { var values = []; // this will contain the all the float values in the // file. var counter = 0; if (format == "pdb") { var index = data.indexOf("\nATOM"); while (index != -1) { while (data.slice(index, index + 5) == "\nATOM" || data.slice(index, index + 7) == "\nHETATM") { values[counter++] = parseFloat(data.slice(index + 31, index + 39)); values[counter++] = parseFloat(data.slice(index + 39, index + 47)); values[counter++] = parseFloat(data.slice(index + 47, index + 55)); index = data.indexOf("\n", index + 54); if (data.slice(index, index + 4) == "\nTER") index = data.indexOf("\n", index + 5); } index = data.indexOf("\nATOM", index); } } else if (format == "netcdf") { var reader = new NetCDFReader(data); values = [].concat.apply([], reader.getDataVariable('coordinates')); } else if (format == "array" || Array.isArray(data)) { return data.flat(2); } else { let index = data.indexOf("\n"); // remove the first line containing title if (format == 'inpcrd') { index = data.indexOf("\n", index + 1); //remove second line w/#atoms } data = data.slice(index + 1); values = data.match(/\S+/g).map(parseFloat); } return values; }; static parseMolData(data?: string | ArrayBuffer, format: string = "", options?: ParserOptionsSpec) { if (!data) return []; //leave an empty model if (/\.gz$/.test(format)) { //unzip gzipped files format = format.replace(/\.gz$/, ''); try { data = inflateString(data); } catch (err) { console.log(err); } } if (typeof (Parsers[format]) == "undefined") { // let someone provide a file name and get format from extension format = format.split('.').pop(); if (typeof (Parsers[format]) == "undefined") { console.log("Unknown format: " + format); // try to guess correct format from data contents if (data instanceof Uint8Array) { format = "mmtf"; //currently only supported binary format? } else if ((data as string).match(/^@MOLECULE/gm)) { format = "mol2"; } else if ((data as string).match(/^data_/gm) && (data as string).match(/^loop_/gm)) { format = "cif"; } else if ((data as string).match(/^HETATM/gm) || (data as string).match(/^ATOM/gm)) { format = "pdb"; } else if ((data as string).match(/ITEM: TIMESTEP/gm)) { format = "lammpstrj"; } else if ((data as string).match(/^.*\n.*\n.\s*(\d+)\s+(\d+)/gm)) { format = "sdf"; // could look at line 3 } else if ((data as string).match(/^%VERSION\s+VERSION_STAMP/gm)) { format = "prmtop"; } else { format = "xyz"; } console.log("Best guess: " + format); } } var parse = Parsers[format]; var parsedAtoms = parse((data as string), options); return parsedAtoms; }; } /** Atom style specification */ export interface AtomStyleSpec { /** draw bonds as lines */ line?: LineStyleSpec; /** draw atoms as crossed lines (aka stars) */ cross?: CrossStyleSpec; /** draw bonds as capped cylinders */ stick?: StickStyleSpec; /** draw atoms as spheres */ sphere?: SphereStyleSpec; /** draw cartoon representation of secondary structure */ cartoon?: CartoonStyleSpec; /** invisible style for click handling only */ clicksphere?: ClickSphereStyleSpec; }; /** Line style specification */ export interface LineStyleSpec { /** do not show line */ hidden?: boolean; /** *deprecated due to vanishing browser support* */ linewidth?: number; /** colorscheme to use on atoms; overrides color */ colorscheme?: ColorschemeSpec; /** fixed coloring */ color?: ColorSpec; /** Allows the user to provide a function for setting the colorschemes. */ colorfunc?: Function; /** opacity (zero to one), must be the same for all atoms in a model */ opacity?: number; /** wireframe style */ wireframe?: boolean; } /** Cross style specification */ export interface CrossStyleSpec { /** do not show line */ hidden?: boolean; /** *deprecated due to vanishing browser support* */ linewidth?: number; /** radius of cross */ radius?: number; /** scale VDW radius by specified amount */ scale?: number; /** colorscheme to use on atoms; overrides color */ colorscheme?: ColorschemeSpec; /** fixed coloring */ color?: ColorSpec; /** Allows the user to provide a function for setting the colorschemes. */ colorfunc?: Function; /** opacity (zero to one), must be the same for all atoms in a model */ opacity?: number; } /** Dashed Bond style specification */ export interface DashedBondSpec { /** length of dash (default 0.1) */ dashLength?: number; /** length of gap (default 0.25) */ gapLength?: number; } /** Stick (cylinder) style specification */ export interface StickStyleSpec { /** do not show sticks */ hidden?: boolean; /** radius of stick */ radius?: number; /** radius scaling factor for drawing double bonds (default 0.4) */ doubleBondScaling?: number; /** radius scaling factor for drawing triple bonds (default 0.25) */ tripleBondScaling?: number; /** dashed bond properties */ dashedBondConfig?: DashedBondSpec; /** draw all bonds as dashed bonds */ dashedBonds?: boolean; /** draw all bonds as single bonds */ singleBonds?: boolean; /** colorscheme to use on atoms; overrides color */ colorscheme?: ColorschemeSpec; /** fixed coloring */ color?: ColorSpec; /** Allows the user to provide a function for setting the colorschemes. */ colorfunc?: Function; /** opacity (zero to one), must be the same for all atoms in a model */ opacity?: number; /** display nonbonded atoms as spheres */ showNonBonded?: boolean; } /** Sphere (spacefill) style specification */ export interface SphereStyleSpec { /** do not show sticks */ hidden?: boolean; /** fixed radius of sphere */ radius?: number; /** scale VDW radius by specified amount */ scale?: number; /** colorscheme to use on atoms; overrides color */ colorscheme?: ColorschemeSpec; /** fixed coloring */ color?: ColorSpec; /** Allows the user to provide a function for setting the colorschemes. */ colorfunc?: Function; /** opacity (zero to one), must be the same for all atoms in a model */ opacity?: number; } /** Invisible click sphere style specification. This lets you set * larger (or smaller) click targets on atoms then the default radii or * have clickable atoms even if they aren't being rendered visibly. */ export interface ClickSphereStyleSpec { /** do not show sticks */ hidden?: boolean; /** fixed radius of sphere */ radius?: number; /** scale VDW radius by specified amount */ scale?: number; } /** Style for individual bond. */ export interface BondStyle { iswire?: boolean; /** */ singleBond?: boolean; /** */ radius?: number; /** */ color1?: ColorSpec; /** */ color2?: ColorSpec; }