// 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, inflateString } from "./utilities";
import { Gradient } from "./Gradient";
import { Parsers } from "./parsers";
import { NetCDFReader } from "netcdfjs"
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";
/**
* 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<number, Geometry>} geos
*/
private drawAtomCross(atom: AtomSpec, geos: Record<number, Geometry>) {
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<number,Geometry>} geos
*/
private drawBondLines(atom: AtomSpec, atoms: AtomSpec[], geos: Record<number, Geometry>) {
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<number, Geometry> = {};
var crossGeometries: Record<number, Geometry> = {};
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<Matrix4>}
*
*/
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<Matrix4>} 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<void>(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<string>(["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 (<root>/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 {
if (format.match(/bcif/i)) {
data = inflateString(data, false);
} else {
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 = "bcif"; //mmtf deprecated so go with bcif
} else if ((data as string).match(/^@<TRIPOS>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;
}