// 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, RingImposterMaterial, 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 viewer: GLViewer; private readonly defaultSphereRadius: number; private readonly defaultCartoonQuality: number; // bonds as cylinders private readonly defaultStickRadius = 0.25; private _drawnAromaticRings: Set = new Set(); private _ringCache: Map = new Map(); constructor(mid, options?, viewer?) { this.options = options || {}; this.viewer = viewer; 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 { // pick a distinct neighbor of atom2 (not atom) to define an offset direction atom3 = null; for (i2 = 0; i2 < atom2.bonds.length; i2++) { j2 = atom2.bonds[i2]; if (j2 !== atom.index) { atom3 = this.atoms[j2]; break; } } if (!atom3) { // degenerate case: no distinct neighbor found v = dir.clone(); v.x += 1; } else { 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(); // Canonicalize v direction: always point toward positive x (or y, or z) // This ensures consistent results regardless of which neighbor was used var threshold = 0.001; if (Math.abs(v.x) > threshold) { if (v.x < 0) v.negate(); } else if (Math.abs(v.y) > threshold) { if (v.y < 0) v.negate(); } else if (v.z < 0) { v.negate(); } 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); }; // 3D-aware ring imposter using analytical annulus test. // Encodes the ring plane normal so the fragment shader can project // correctly when the ring is tilted relative to the camera. // // Encoding (4 vertices per ring): // position = ring center (same for all 4) // normal = ringNormal * minorR (direction = ring normal, length = minorR) // color = (r, g, ±b) sign(b) = Y billboard corner // radius = ±outerR sign = X billboard corner private drawRingImposter(geo: Geometry, center: XYZ, ringNormal: XYZ, majorRadius: number, minorRadius: number, C: Color) { var geoGroup = geo.updateGeoGroup(4); var startv = geoGroup.vertices; var start = startv * 3; var vertexArray = geoGroup.vertexArray; var colorArray = geoGroup.colorArray; var normalArray = geoGroup.normalArray; var radiusArray = geoGroup.radiusArray; var outerR = majorRadius + minorRadius; // All 4 vertices share the same center position for (var 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; } // Normal = ringNormal scaled by minorR (direction + magnitude encoding) var scaledNx = ringNormal.x * minorRadius; var scaledNy = ringNormal.y * minorRadius; var scaledNz = ringNormal.z * minorRadius; for (var i = 0; i < 4; i++) { normalArray[start + 3 * i] = scaledNx; normalArray[start + 3 * i + 1] = scaledNy; normalArray[start + 3 * i + 2] = scaledNz; } // Color: RGB with sign of blue encoding Y billboard corner // vertices 0,3: +b (Y = +outerR) // vertices 1,2: -b (Y = -outerR) var b = Math.abs(C.b); if (b < 0.0001) b = 0.0001; // ensure nonzero so sign is meaningful colorArray[start + 0] = C.r; colorArray[start + 1] = C.g; colorArray[start + 2] = b; // vertex 0: Y+ colorArray[start + 3] = C.r; colorArray[start + 4] = C.g; colorArray[start + 5] = -b; // vertex 1: Y- colorArray[start + 6] = C.r; colorArray[start + 7] = C.g; colorArray[start + 8] = -b; // vertex 2: Y- colorArray[start + 9] = C.r; colorArray[start + 10] = C.g; colorArray[start + 11] = b; // vertex 3: Y+ // Radius: ±outerR, sign encodes X billboard corner // vertices 0,1: -outerR (X = -outerR) // vertices 2,3: +outerR (X = +outerR) radiusArray[startv + 0] = -outerR; // vertex 0: X- radiusArray[startv + 1] = -outerR; // vertex 1: X- radiusArray[startv + 2] = outerR; // vertex 2: X+ radiusArray[startv + 3] = outerR; // vertex 3: X+ geoGroup.vertices += 4; // Two faces var faceArray = geoGroup.faceArray; var faceoffset = geoGroup.faceidx; 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; }; /** * Calculate dashed line segments along a bond, with proper centering and two-color support. * * The dashes are centered along the bond using the pattern: * [halfGap]-[dash]-[gap]-[dash]-[gap]-...-[dash]-[halfGap] * * For two-color bonds, the first half of dashes use colors[0], second half use colors[1]. */ private calculateDashes(from: XYZ, to: XYZ, radius: number, dashLength: number, gapLength: number, colors?: Color[]) { const dx = to.x - from.x; const dy = to.y - from.y; const dz = to.z - from.z; const bondLength = Math.sqrt(dx * dx + dy * dy + dz * dz); // Handle degenerate case: zero or near-zero length bond if (bondLength < 0.001) { return [{ from: new Vector3(from.x, from.y, from.z), to: new Vector3(to.x, to.y, to.z), color: colors?.[0] ?? null }]; } // Clamp inputs to valid ranges radius = Math.max(radius, 0); dashLength = Math.max(dashLength, 0.001); // Effective gap includes cylinder radius on each side to prevent visual overlap const effectiveGap = Math.max(gapLength, 0) + 2 * radius; // Minimum padding at each end to prevent dashes from touching bond endpoints const endPadding = effectiveGap / 2; const drawableLength = bondLength - 2 * endPadding; // If drawable region is too small, just draw a single segment if (drawableLength < dashLength) { return [{ from: new Vector3(from.x, from.y, from.z), to: new Vector3(to.x, to.y, to.z), color: colors?.[0] ?? null }]; } // Calculate how many dashes fit in the drawable region const numDashes = Math.floor((drawableLength - dashLength) / (dashLength + effectiveGap)) + 1; const totalDashLength = numDashes * dashLength; // Redistribute remaining space evenly as gaps within drawable region // Pattern: [dash][gap][dash][gap]...[dash] // Number of gaps between dashes = numDashes - 1 // We want to center this pattern within drawableLength const totalInternalGaps = numDashes > 1 ? numDashes - 1 : 0; const remainingSpace = drawableLength - totalDashLength; const internalGap = totalInternalGaps > 0 ? remainingSpace / (totalInternalGaps + 1) : remainingSpace; const halfGap = internalGap / 2; // Normalized direction vector const dirX = dx / bondLength; const dirY = dy / bondLength; const dirZ = dz / bondLength; const segments: Array<{ from: Vector3; to: Vector3; color: Color | null }> = []; // Start after endPadding + halfGap to center dashes in drawable region const startPosition = endPadding + halfGap; for (let i = 0; i < numDashes; i++) { const startOffset = startPosition + i * (dashLength + internalGap); const endOffset = startOffset + dashLength; const dashStart = new Vector3( from.x + dirX * startOffset, from.y + dirY * startOffset, from.z + dirZ * startOffset ); const dashEnd = new Vector3( from.x + dirX * endOffset, from.y + dirY * endOffset, from.z + dirZ * endOffset ); // Two-color support: first half uses colors[0], second half uses colors[1] const useSecondColor = colors && colors.length > 1 && i >= numDashes / 2; const color = useSecondColor ? colors[1] : (colors?.[0] ?? null); segments.push({ from: dashStart, to: dashEnd, color }); } return segments; } // Calculate segments for solid two-color bonds (split at midpoint) private calculateTwoColorSegments(from: XYZ, to: XYZ, colors: Color[]) { const segments = []; if (colors.length <= 1) { segments.push({ from: new Vector3(from.x, from.y, from.z), to: new Vector3(to.x, to.y, to.z), color: colors[0] }); } else { const mp = new Vector3((from.x + to.x) / 2, (from.y + to.y) / 2, (from.z + to.z) / 2); segments.push({ from: new Vector3(from.x, from.y, from.z), to: mp, color: colors[0] }); segments.push({ from: mp, to: new Vector3(to.x, to.y, to.z), color: colors[1] }); } return segments; } // BFS for the smallest ring containing the bond (atomIdx1–atomIdx2). // Results are cached per render pass in _ringCache. private findSmallestRing(atomIdx1: number, atomIdx2: number, maxRingSize: number = 8): number[] | null { const lo = atomIdx1 < atomIdx2 ? atomIdx1 : atomIdx2; const hi = atomIdx1 < atomIdx2 ? atomIdx2 : atomIdx1; const cacheKey = lo + "," + hi; if (this._ringCache.has(cacheKey)) return this._ringCache.get(cacheKey); const queue: number[][] = [[atomIdx1]]; const visited = new Set([atomIdx1]); while (queue.length > 0) { const path = queue.shift(); const current = path[path.length - 1]; // Ring would be path.length atoms + the direct bond back if (path.length >= maxRingSize) continue; const atom = this.atoms[current]; if (!atom || !atom.bonds) continue; for (let i = 0; i < atom.bonds.length; i++) { const neighbor = atom.bonds[i]; // Skip the direct bond between the two endpoints if (current === atomIdx1 && neighbor === atomIdx2) continue; if (current === atomIdx2 && neighbor === atomIdx1) continue; if (neighbor === atomIdx2 && path.length >= 2) { const ring = [...path, atomIdx2]; this._ringCache.set(cacheKey, ring); return ring; } if (!visited.has(neighbor)) { visited.add(neighbor); queue.push([...path, neighbor]); } } } this._ringCache.set(cacheKey, null); return null; } // Choose which side of a multi-bond gets the dashed line. // For ring bonds, places dashes toward the ring interior; otherwise falls back // to the neighbor-centroid heuristic. private chooseDashedSide(atom: AtomSpec, atom2: AtomSpec, p1: Vector3, p2: Vector3, v: Vector3): boolean { const ring = this.findSmallestRing(atom.index, atom2.index); if (ring && ring.length >= 3) { // Bond is in a ring - use ring centroid for dash side const centroid = new Vector3(0, 0, 0); for (let ri = 0; ri < ring.length; ri++) { const ra = this.atoms[ring[ri]]; centroid.x += ra.x; centroid.y += ra.y; centroid.z += ra.z; } centroid.multiplyScalar(1.0 / ring.length); const mid = new Vector3().addVectors(p1, p2).multiplyScalar(0.5); const toRing = new Vector3( centroid.x - mid.x, centroid.y - mid.y, centroid.z - mid.z ); const dot = v.x * toRing.x + v.y * toRing.y + v.z * toRing.z; const vLen = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); const toLen = Math.sqrt(toRing.x * toRing.x + toRing.y * toRing.y + toRing.z * toRing.z); if (toLen >= 0.001 && Math.abs(dot) >= 0.05 * vLen * toLen) { return dot > 0; } // Ring centroid is nearly on the bond axis - fall back to neighbor heuristic } // No ring or degenerate ring geometry - use neighbor heuristic return this.chooseDashedPlusV(atom, atom2, p1, p2, v); } // Determine which side of a multi-bond should have the dashed line. // Returns true if dashed should be on the +v side (toward neighbors). // Uses immediate neighbor positions for stability across conformations. private chooseDashedPlusV(atom: AtomSpec, atom2: AtomSpec, p1: Vector3, p2: Vector3, v: Vector3) { const neighborCenter = new Vector3(0, 0, 0); let count = 0; // Add atom's neighbors (except atom2) for (let i = 0; i < atom.bonds.length; i++) { if (atom.bonds[i] === atom2.index) continue; const neighbor = this.atoms[atom.bonds[i]]; if (neighbor) { neighborCenter.x += neighbor.x; neighborCenter.y += neighbor.y; neighborCenter.z += neighbor.z; count++; } } // Add atom2's neighbors (except atom) for (let i = 0; i < atom2.bonds.length; i++) { if (atom2.bonds[i] === atom.index) continue; const neighbor = this.atoms[atom2.bonds[i]]; if (neighbor) { neighborCenter.x += neighbor.x; neighborCenter.y += neighbor.y; neighborCenter.z += neighbor.z; count++; } } if (count === 0) return false; neighborCenter.multiplyScalar(1.0 / count); const mid = new Vector3().addVectors(p1, p2).multiplyScalar(0.5); const toNeighbors = new Vector3( neighborCenter.x - mid.x, neighborCenter.y - mid.y, neighborCenter.z - mid.z ); const dot = v.x * toNeighbors.x + v.y * toNeighbors.y + v.z * toNeighbors.z; // If nearly perpendicular, use canonical v direction const vLen = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); const toLen = Math.sqrt(toNeighbors.x * toNeighbors.x + toNeighbors.y * toNeighbors.y + toNeighbors.z * toNeighbors.z); if (toLen < 0.001 || Math.abs(dot) < 0.1 * vLen * toLen) { return false; // Use canonical v direction } return dot > 0; } // Draw a torus inside the aromatic ring that contains this bond. // Each ring is drawn once per render pass (deduplicated by sorted atom indices). private drawAromaticRingTorus(atom: AtomSpec, atom2: AtomSpec, geo: Geometry, bondR: number, color: Color) { const ring = this.findSmallestRing(atom.index, atom2.index); if (!ring || ring.length < 3) return; const ringKey = ring.slice().sort((a, b) => a - b).join(","); if (this._drawnAromaticRings.has(ringKey)) return; this._drawnAromaticRings.add(ringKey); // centroid const centroid = new Vector3(0, 0, 0); for (let ri = 0; ri < ring.length; ri++) { const ra = this.atoms[ring[ri]]; centroid.x += ra.x; centroid.y += ra.y; centroid.z += ra.z; } centroid.multiplyScalar(1.0 / ring.length); // ring-plane normal (Newell's method) const normal = new Vector3(0, 0, 0); for (let ri = 0; ri < ring.length; ri++) { const cur = this.atoms[ring[ri]]; const next = this.atoms[ring[(ri + 1) % ring.length]]; normal.x += (cur.y - next.y) * (cur.z + next.z); normal.y += (cur.z - next.z) * (cur.x + next.x); normal.z += (cur.x - next.x) * (cur.y + next.y); } const nLen = Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z); if (nLen < 1e-4) return; // degenerate (collinear) ring normal.multiplyScalar(1.0 / nLen); // average distance from centroid to ring atoms let avgDist = 0; let maxSphereR = 0; for (let ri = 0; ri < ring.length; ri++) { const ra = this.atoms[ring[ri]]; const dx = ra.x - centroid.x; const dy = ra.y - centroid.y; const dz = ra.z - centroid.z; avgDist += Math.sqrt(dx * dx + dy * dy + dz * dz); if (ra.style?.sphere && !ra.style.sphere.hidden && ra.style.sphere.radius) { const sr = this.getRadiusFromStyle(ra, ra.style.sphere); if (sr > maxSphereR) maxSphereR = sr; } } avgDist /= ring.length; const minorRadius = bondR * 0.5; // Two clearance constraints: // 1) Bond sticks: torus outer edge + bondR gap < apothem // majorRadius + minorRadius + bondR < apothem // 2) Atom spheres: torus outer edge + bondR gap < avgDist - sphereR // majorRadius + minorRadius + bondR < avgDist - sphereR const apothem = avgDist * Math.cos(Math.PI / ring.length); const bondLimit = apothem - minorRadius - 2 * bondR; const sphereLimit = avgDist - minorRadius - maxSphereR - bondR; const majorRadius = Math.max(minorRadius, Math.min(bondLimit, sphereLimit)); if (geo.imposter) { this.drawRingImposter(geo, centroid, normal, majorRadius, minorRadius, color); } else { GLDraw.drawTorus(geo, centroid, normal, majorRadius, minorRadius, color); } } 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, torusGeo?: 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; const bondDashLength = style.dashedBondConfig?.dashLength || 0.1; const bondGapLength = style.dashedBondConfig?.gapLength || 0.25; const aromaticStyle = style.aromaticStyle || "dashed"; var bondR = atomBondR; var atomSingleBond = style.singleBonds || false; var atomDashedBonds = style.dashedBonds || false; var fromCap = 0, toCap = 0; var atomneedsi, atom2needsi, i, singleBond; var cylinder1a, cylinder1b, cylinder1c, cylinder2a, cylinder2b, cylinder2c; var atomColor = getColorFromStyle(atom, style); var mp, mp2, mp3; if (!atom.capDrawn && atom.bonds.length < 4) fromCap = 2; // Get custom colors from dashedBondConfig if specified const solidColor = style.dashedBondConfig?.solidColor ? CC.color(style.dashedBondConfig.solidColor) as Color : null; const dashedColor = style.dashedBondConfig?.dashedColor ? CC.color(style.dashedBondConfig.dashedColor) as Color : null; const selectCylDrawMethod = (bondOrder) => { const drawMethod = geo.imposter ? GLModel.drawStickImposter : GLDraw.drawCylinder; if (!atomDashedBonds && bondOrder % 1 === 0) { // Integer bond order - solid return (geo, from, to, radius, color1, color2, fromCap = 0, toCap = 0) => { if (!color2 || color1 === color2) { drawMethod(geo, from, to, radius, color1, fromCap, toCap); } else { const segments = this.calculateTwoColorSegments(from, to, [color1, color2]); for (let si = 0; si < segments.length; si++) { const seg = segments[si]; const fc = (si === 0) ? fromCap : 0; const tc = (si === segments.length - 1) ? toCap : 0; drawMethod(geo, seg.from, seg.to, radius, seg.color, fc, tc); } } }; } // Fractional bond order - dashed return (geo, from, to, radius, color1, color2, fromCap = 0, toCap = 0) => { const colors = color2 ? [color1, color2] : [color1]; const segments = this.calculateDashes(from, to, radius, bondDashLength, bondGapLength, colors); segments.forEach(segment => { drawMethod(geo, segment.from, segment.to, radius, segment.color, fromCap, toCap); }); }; }; for (i = 0; i < atom.bonds.length; i++) { const rawBondOrder = atom.bondOrder[i]; const isAromatic = rawBondOrder === 4; let renderBondOrder: number; if (isAromatic) { // "circle" draws a single bond + torus per ring // "dashed" draws one solid + one dashed bond (1.5 order) renderBondOrder = (aromaticStyle === "circle") ? 1 : 1.5; } else { renderBondOrder = rawBondOrder; } const drawCyl = selectCylDrawMethod(renderBondOrder); const j = atom.bonds[i]; // our neighbor const 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 toCap = 0; const style2 = atom2.style; if (!style2.stick || style2.stick.hidden) continue; // don't sweat the details let C1 = atomColor; let C2 = getColorFromStyle(atom2, style2.stick); //support bond specific styles bondR = atomBondR; singleBond = atomSingleBond; const bstyle = atom.bondStyles?.[i]; if (bstyle) { 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; } } const p1 = new Vector3(atom.x, atom.y, atom.z); const p2 = new Vector3(atom2.x, atom2.y, atom2.z); // Determine colors and dash geometry for solid/dashed portions // Priority: per-bond dashedBondConfig > per-bond color1/color2 > global dashedBondConfig > atom color const perBondSolid = bstyle?.dashedBondConfig?.solidColor ? CC.color(bstyle.dashedBondConfig.solidColor) as Color : null; const perBondDashed = bstyle?.dashedBondConfig?.dashedColor ? CC.color(bstyle.dashedBondConfig.dashedColor) as Color : null; const hasPerBondColor = bstyle && (typeof bstyle.color1 !== "undefined" || typeof bstyle.color2 !== "undefined"); const solid1 = perBondSolid || (hasPerBondColor ? C1 : (solidColor || C1)); const solid2 = perBondSolid || (hasPerBondColor ? C2 : (solidColor || C2)); const dashed1 = perBondDashed || (hasPerBondColor ? C1 : (dashedColor || C1)); const dashed2 = perBondDashed || (hasPerBondColor ? C2 : (dashedColor || C2)); const perBondDashLength = bstyle?.dashedBondConfig?.dashLength || bondDashLength; const perBondGapLength = bstyle?.dashedBondConfig?.gapLength || bondGapLength; // draw cylinders if (renderBondOrder <= 1 || singleBond || renderBondOrder > 3) { if (renderBondOrder < 1) bondR *= renderBondOrder; if (!atom2.capDrawn && atom2.bonds.length < 4) toCap = 2; const isDashed = renderBondOrder % 1 !== 0; const colA = isDashed ? dashed1 : C1; const colB = isDashed ? dashed2 : C2; drawCyl(geo, p1, p2, bondR, colA, colB, fromCap, toCap); 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); } } if (isAromatic && aromaticStyle === "circle") { const torusColor = perBondDashed || dashedColor || C1; this.drawAromaticRingTorus(atom, atom2, torusGeo, bondR, torusColor); } } else if (renderBondOrder > 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; } const dir = p2.clone(); dir.sub(p1); let r, r2, p1a, p1b, p2a, p2b; const v = this.getSideBondV(atom, atom2, i); // Determine dashed bond placement for fractional orders (aromatic) let isDashedBondFlipped = false; if (renderBondOrder % 1 !== 0) { if (bstyle?.dashedBondFlip != null) { isDashedBondFlipped = bstyle.dashedBondFlip; } else { isDashedBondFlipped = this.chooseDashedSide(atom, atom2, p1, p2, v); } } const multiBondDrawMethod = geo.imposter ? GLModel.drawStickImposter : GLDraw.drawCylinder; const drawSolidHalf = (from, to, radius, c1, c2, fc, tc) => { if (c1 != c2) { const m = new Vector3().addVectors(from, to).multiplyScalar(0.5); multiBondDrawMethod(geo, from, m, radius, c1, fc, 0); multiBondDrawMethod(geo, m, to, radius, c2, 0, tc); } else { multiBondDrawMethod(geo, from, to, radius, c1, fc, tc); } }; const drawDashedHalf = (from, to, radius, c1, c2, fc, tc) => { const colors = (c1 != c2) ? [c1, c2] : [c1]; const segments = this.calculateDashes(from, to, radius, perBondDashLength, perBondGapLength, colors); segments.forEach(segment => { multiBondDrawMethod(geo, segment.from, segment.to, radius, segment.color || c1, fc, tc); }); }; if (renderBondOrder > 1 && renderBondOrder <= 2) { // Fractional double bond (aromatic, 1.5, etc) r = bondR * doubleBondScale; r2 = r * (renderBondOrder - 1); // dashed bond is thinner 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 (renderBondOrder === 2) { // Integer double bond - both solid drawSolidHalf(p1a, p2a, r, C1, C2, mfromCap, mtoCap); drawSolidHalf(p1b, p2b, r, C1, C2, mfromCap, mtoCap); } else { // Fractional double bond (1.x) - one solid, one dashed if (!isDashedBondFlipped) { drawSolidHalf(p1a, p2a, r, solid1, solid2, mfromCap, mtoCap); drawDashedHalf(p1b, p2b, r2, dashed1, dashed2, mfromCap, mtoCap); } else { drawDashedHalf(p1a, p2a, r2, dashed1, dashed2, mfromCap, mtoCap); drawSolidHalf(p1b, p2b, r, solid1, solid2, 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); // Account for flip - dashed bond has radius r2 const raRadius = (renderBondOrder === 2) ? r : (isDashedBondFlipped ? r2 : r); const rbRadius = (renderBondOrder === 2) ? r : (isDashedBondFlipped ? r : r2); if (atomneedsi) { cylinder1a = new Cylinder(p1a, mp, raRadius); cylinder1b = new Cylinder(p1b, mp2, rbRadius); atom.intersectionShape.cylinder.push(cylinder1a); atom.intersectionShape.cylinder.push(cylinder1b); } if (atom2needsi) { cylinder2a = new Cylinder(p2a, mp, raRadius); cylinder2b = new Cylinder(p2b, mp2, rbRadius); atom2.intersectionShape.cylinder.push(cylinder2a); atom2.intersectionShape.cylinder.push(cylinder2b); } } } else if (renderBondOrder > 2 && renderBondOrder <= 3) { r = bondR * tripleBondScale; const r3 = r * (renderBondOrder - 2); // dashed bond thinner for fractional 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 (renderBondOrder === 3) { // Integer triple bond - all solid drawSolidHalf(p1a, p2a, r, C1, C2, mfromCap, mtoCap); drawSolidHalf(p1, p2, r, C1, C2, fromCap, toCap); drawSolidHalf(p1b, p2b, r, C1, C2, mfromCap, mtoCap); } else { // Fractional triple bond (2.x) - one dashed if (!isDashedBondFlipped) { drawSolidHalf(p1a, p2a, r, solid1, solid2, mfromCap, mtoCap); drawSolidHalf(p1, p2, r, solid1, solid2, fromCap, toCap); drawDashedHalf(p1b, p2b, r3, dashed1, dashed2, mfromCap, mtoCap); } else { drawDashedHalf(p1a, p2a, r3, dashed1, dashed2, mfromCap, mtoCap); drawSolidHalf(p1, p2, r, solid1, solid2, fromCap, toCap); drawSolidHalf(p1b, p2b, r, solid1, solid2, 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); // Account for flip - dashed bond has radius r3 const raRadius = (renderBondOrder === 3) ? r : (isDashedBondFlipped ? r3 : r); const rbRadius = (renderBondOrder === 3) ? r : (isDashedBondFlipped ? r : r3); if (atomneedsi) { cylinder1a = new Cylinder(p1a.clone(), mp.clone(), raRadius); cylinder1b = new Cylinder(p1b.clone(), mp2.clone(), rbRadius); 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(), raRadius); cylinder2b = new Cylinder(p2b.clone(), mp2.clone(), rbRadius); 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; var bstyle = atom.bondStyles?.[i]; if (bstyle) { 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, atomColor); } else { GLDraw.drawSphere(geo, atom, bondR, atomColor); } } }; // 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 || {}; this._drawnAromaticRings = new Set(); this._ringCache = new Map(); var ret = new Object3D(); var cartoonAtoms = []; var lineGeometries: Record = {}; var crossGeometries: Record = {}; var drawSphereFunc = this.drawAtomSphere; var sphereGeometry: Geometry = null; var stickGeometry: Geometry = null; var torusGeometry: Geometry = null; if (options.supportsImposters) { torusGeometry = new Geometry(true); torusGeometry.imposter = true; torusGeometry.radii = true; // radiusArray encodes outerR + billboard corner 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) { torusGeometry = new Geometry(true); drawSphereFunc = this.drawAtomInstanced; sphereGeometry = new Geometry(false, true, true); sphereGeometry.instanced = true; stickGeometry = new Geometry(true); //don't actually have instanced sticks } else { torusGeometry = new Geometry(true); 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, torusGeometry); 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); } } if (torusGeometry.vertices > 0) { torusGeometry.initTypedArrays(); var torusMaterial: any; if (torusGeometry.imposter) { torusMaterial = new RingImposterMaterial({ ambient: 0x000000, vertexColors: true, reflectivity: 0 }); } else { torusMaterial = new MeshLambertMaterial({ ambient: 0x000000, vertexColors: true, reflectivity: 0 }); } if (opacities.stick < 1 && opacities.stick >= 0) { torusMaterial.transparent = true; torusMaterial.opacity = opacities.stick; } ret.add(new Mesh(torusGeometry, torusMaterial as Material)); } //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 viewer associated with this model. * @returns GLViewer */ public getViewer() { return this.viewer; } /** * 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; this.molObj = null; } }; /** * 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) { var numFrames = this.getNumFrames(); let model = this; let viewer = this.viewer; 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 {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, arrowSpec?: ArrowSpec) { var start = 0; var end = numFrames; let viewer = this.viewer; 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] || {}; 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 // Build frames and modelDatas in one pass, skipping empty frames. // modelDatas (from SDF parsing) uses raw parsed indices, so we // compact it here to stay aligned with this.frames. const remapped = this.modelDatas ? [] as any[] : null; for (let i = 0; i < parsedAtoms.length; i++) { if (parsedAtoms[i].length != 0) { this.frames.push(parsedAtoms[i]); if (remapped) remapped.push(this.modelDatas[i] || null); } } if (remapped) { this.modelDatas = remapped; if (remapped[0]) this.modelData = remapped[0]; } 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); } // Apply embedded 3DMOL_STYLE model-level styles from SDF parsing. // modelDatas was already remapped to match this.frames indices above. if (this.modelDatas) { for (let i = 0; i < this.modelDatas.length; i++) { if (this.modelDatas[i]?.style) { this.setStyle({frame: i}, this.modelDatas[i].style, true); } } } else if (mData && mData.style) { this.setStyle({}, mData.style, true); } 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 || selobject instanceof GLModel) 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 (key == "model") { let m = sel.model; if(typeof m === 'number' && m < 0) { m = this.viewer.getNextModelId()+m; } if (Array.isArray(m)) { // handle list let isokay = false; for (let i = 0; i < m.length; i++) { if(m[i] == this || m[i] == this.id) { isokay = true; break; } } if (!isokay) { ret = false; break; } } else { if(m != this && m != this.id) { 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; } let 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 {LabelSpec} options */ public addPropertyLabels(prop: string, sel: AtomSelectionSpec, style: LabelSpec) { let atoms = this.selectedAtoms(sel); let viewer = this.viewer; let mystyle = deepCopy(style); for (let i = 0; i < atoms.length; i++) { let 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 {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, style: LabelSpec, byframe: boolean = false) { let viewer = this.viewer; 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 { 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(/^@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; /** Color for the solid portion of aromatic bonds (default: atom color) */ solidColor?: ColorSpec; /** Color for the dashed portion of aromatic bonds (default: atom color) */ dashedColor?: ColorSpec; } /** 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; /** Style for rendering aromatic (bond order 4) bonds. * "dashed" = one solid + one dashed bond (default) * "circle" = single bond + circle/torus inside the aromatic ring */ aromaticStyle?: "dashed" | "circle"; } /** 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; /** Override which side of a fractional bond (e.g. aromatic 1.5) gets the dashed line. * true = dashed on +v side, false = dashed on -v side. * When absent, automatically determined via ring detection. */ dashedBondFlip?: boolean; /** Override dashed bond colors for this specific bond */ dashedBondConfig?: DashedBondSpec; }