3dmol
Version:
JavaScript/TypeScript molecular visualization library
892 lines (777 loc) • 223 kB
HTML
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="Author" content="David Koes & 3Dmoljs Contributors"><meta name="Description" content="A modern, object-oriented JavaScript library for visualizing molecular data"><title>Source: GLViewer.ts</title><!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><style>article ul li{list-style:disc}</style><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="light"><div class="sidebar-container"><div class="sidebar" id="sidebar"><a href="/" class="sidebar-title sidebar-title-anchor">3Dmol.js</a><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="mei5HJjYKLn6aua5Z5PM5"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="$3Dmol.Label.html">Label</a></div><div class="sidebar-section-children"><a href="$3Dmol.StateManager.html">StateManager</a></div><div class="sidebar-section-children"><a href="$3Dmol.UI.html">UI</a></div><div class="sidebar-section-children"><a href="Color.html">Color</a></div><div class="sidebar-section-children"><a href="CustomLinear.html">CustomLinear</a></div><div class="sidebar-section-children"><a href="Cylinder.html">Cylinder</a></div><div class="sidebar-section-children"><a href="GLModel.html">GLModel</a></div><div class="sidebar-section-children"><a href="GLShape.html">GLShape</a></div><div class="sidebar-section-children"><a href="GLShape_GLShape.html">GLShape</a></div><div class="sidebar-section-children"><a href="GLViewer.html">GLViewer</a></div><div class="sidebar-section-children"><a href="GLVolumetricRender.html">GLVolumetricRender</a></div><div class="sidebar-section-children"><a href="Matrix3.html">Matrix3</a></div><div class="sidebar-section-children"><a href="Matrix4.html">Matrix4</a></div><div class="sidebar-section-children"><a href="Quaternion.html">Quaternion</a></div><div class="sidebar-section-children"><a href="ROYGB.html">ROYGB</a></div><div class="sidebar-section-children"><a href="RWB.html">RWB</a></div><div class="sidebar-section-children"><a href="Ray.html">Ray</a></div><div class="sidebar-section-children"><a href="Sinebow.html">Sinebow</a></div><div class="sidebar-section-children"><a href="Sphere.html">Sphere</a></div><div class="sidebar-section-children"><a href="Surface.html">Surface</a></div><div class="sidebar-section-children"><a href="Triangle.html">Triangle</a></div><div class="sidebar-section-children"><a href="Vector2.html">Vector2</a></div><div class="sidebar-section-children"><a href="Vector3.html">Vector3</a></div><div class="sidebar-section-children"><a href="VolumeData.html">VolumeData</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="kv4Yx5z0THf_b8XR4Gje1"><div>Namespaces</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="GLDraw.html">GLDraw</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="ykThSufQWJcKGT1kXzr-A"><div>Tutorials</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="tutorial-code.html">Using 3Dmol within your code</a></div><div class="sidebar-section-children"><a href="tutorial-embeddable.html">Embedding a 3Dmol Viewer</a></div><div class="sidebar-section-children"><a href="tutorial-home.html">3Dmol Tutorials - Home</a></div><div class="sidebar-section-children"><a href="tutorial-learning_environment.html">Active Learning with 3Dmol.js</a></div><div class="sidebar-section-children"><a href="tutorial-url.html">Viewing Molecules with the 3Dmol Server</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="XcP4RUEg6tp7YKHbBVi9E"><div>Interfaces</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="AmbientOcclusionStyle.html">AmbientOcclusionStyle</a></div><div class="sidebar-section-children"><a href="ArrowSpec.html">ArrowSpec</a></div><div class="sidebar-section-children"><a href="AtomSelectionSpec.html">AtomSelectionSpec</a></div><div class="sidebar-section-children"><a href="AtomSpec.html">AtomSpec</a></div><div class="sidebar-section-children"><a href="AtomStyleSpec.html">AtomStyleSpec</a></div><div class="sidebar-section-children"><a href="BondStyle.html">BondStyle</a></div><div class="sidebar-section-children"><a href="BoxSpec.html">BoxSpec</a></div><div class="sidebar-section-children"><a href="CartoonStyleSpec.html">CartoonStyleSpec</a></div><div class="sidebar-section-children"><a href="ClickSphereStyleSpec.html">ClickSphereStyleSpec</a></div><div class="sidebar-section-children"><a href="CrossStyleSpec.html">CrossStyleSpec</a></div><div class="sidebar-section-children"><a href="CurveSpec.html">CurveSpec</a></div><div class="sidebar-section-children"><a href="CustomShapeSpec.html">CustomShapeSpec</a></div><div class="sidebar-section-children"><a href="CylinderSpec.html">CylinderSpec</a></div><div class="sidebar-section-children"><a href="DashedBondSpec.html">DashedBondSpec</a></div><div class="sidebar-section-children"><a href="FogSpec.html">FogSpec</a></div><div class="sidebar-section-children"><a href="IsoSurfaceSpec.html">IsoSurfaceSpec</a></div><div class="sidebar-section-children"><a href="LabelSpec.html">LabelSpec</a></div><div class="sidebar-section-children"><a href="LineSpec.html">LineSpec</a></div><div class="sidebar-section-children"><a href="LineStyleSpec.html">LineStyleSpec</a></div><div class="sidebar-section-children"><a href="OutlineStyle.html">OutlineStyle</a></div><div class="sidebar-section-children"><a href="ParserOptionsSpec.html">ParserOptionsSpec</a></div><div class="sidebar-section-children"><a href="ShapeSpec.html">ShapeSpec</a></div><div class="sidebar-section-children"><a href="SphereSpec.html">SphereSpec</a></div><div class="sidebar-section-children"><a href="SphereStyleSpec.html">SphereStyleSpec</a></div><div class="sidebar-section-children"><a href="StickStyleSpec.html">StickStyleSpec</a></div><div class="sidebar-section-children"><a href="SurfaceStyleSpec.html">SurfaceStyleSpec</a></div><div class="sidebar-section-children"><a href="UnitCellStyleSpec.html">UnitCellStyleSpec</a></div><div class="sidebar-section-children"><a href="ViewStyle.html">ViewStyle</a></div><div class="sidebar-section-children"><a href="ViewerGridSpec.html">ViewerGridSpec</a></div><div class="sidebar-section-children"><a href="ViewerSpec.html">ViewerSpec</a></div><div class="sidebar-section-children"><a href="VolumetricRendererSpec.html">VolumetricRendererSpec</a></div><div class="sidebar-section-children"><a href="WithinSelectionSpec.html">WithinSelectionSpec</a></div><div class="sidebar-section-children"><a href="global.html#XYZ">XYZ</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="jmKnIXRJjdjZ366ci7cdd"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#BCIF">BCIF</a></div><div class="sidebar-section-children"><a href="global.html#CAP">CAP</a></div><div class="sidebar-section-children"><a href="global.html#CDJSON">CDJSON</a></div><div class="sidebar-section-children"><a href="global.html#CIF">CIF</a></div><div class="sidebar-section-children"><a href="global.html#CUBE">CUBE</a></div><div class="sidebar-section-children"><a href="global.html#ColorSpec">ColorSpec</a></div><div class="sidebar-section-children"><a href="global.html#ColorschemeSpec">ColorschemeSpec</a></div><div class="sidebar-section-children"><a href="global.html#GRO">GRO</a></div><div class="sidebar-section-children"><a href="global.html#GradientSpec">GradientSpec</a></div><div class="sidebar-section-children"><a href="global.html#MMTFparser">MMTFparser</a></div><div class="sidebar-section-children"><a href="global.html#MOL2">MOL2</a></div><div class="sidebar-section-children"><a href="global.html#OFFSETS">OFFSETS</a></div><div class="sidebar-section-children"><a href="global.html#PDB">PDB</a></div><div class="sidebar-section-children"><a href="global.html#PQR">PQR</a></div><div class="sidebar-section-children"><a href="global.html#PRMTOP">PRMTOP</a></div><div class="sidebar-section-children"><a href="global.html#SDF">SDF</a></div><div class="sidebar-section-children"><a href="global.html#SurfaceType">SurfaceType</a></div><div class="sidebar-section-children"><a href="global.html#VASP">VASP</a></div><div class="sidebar-section-children"><a href="global.html#XYZ">XYZ</a></div><div class="sidebar-section-children"><a href="global.html#assignPDBBonds">assignPDBBonds</a></div><div class="sidebar-section-children"><a href="global.html#builtinColorSchemes">builtinColorSchemes</a></div><div class="sidebar-section-children"><a href="global.html#builtinGradients">builtinGradients</a></div><div class="sidebar-section-children"><a href="global.html#conversionMatrix3">conversionMatrix3</a></div><div class="sidebar-section-children"><a href="global.html#createViewer">createViewer</a></div><div class="sidebar-section-children"><a href="global.html#createViewerGrid">createViewerGrid</a></div><div class="sidebar-section-children"><a href="global.html#decode">decode</a></div><div class="sidebar-section-children"><a href="global.html#dic">dic</a></div><div class="sidebar-section-children"><a href="global.html#download">download</a></div><div class="sidebar-section-children"><a href="global.html#elementColors">elementColors</a></div><div class="sidebar-section-children"><a href="global.html#get">get</a></div><div class="sidebar-section-children"><a href="global.html#getColorFromStyle">getColorFromStyle</a></div><div class="sidebar-section-children"><a href="global.html#getbin">getbin</a></div><div class="sidebar-section-children"><a href="global.html#parseV3000">parseV3000</a></div><div class="sidebar-section-children"><a href="global.html#setSyncSurface">setSyncSurface</a></div><div class="sidebar-section-children"><a href="global.html#ssColors">ssColors</a></div><div class="sidebar-section-children"><a href="global.html#syncSurface">syncSurface</a></div><div class="sidebar-section-children"><a href="global.html#viewers">viewers</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"><div class="navbar-item"><a id="" href="https://3dmol.org/doc/index.html" target="">Documentation</a></div><div class="navbar-item"><a id="" href="tutorial-home.html" target="">Tutorials</a></div><div class="navbar-item"><a id="" href="https://github.com/3dmol/3Dmol.js" target="_blank">GitHub</a></div></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#dark-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">GLViewer.ts</h1></header><article><pre class="prettyprint source lang-js"><code>//a molecular viewer based on GLMol
import { decode, encode, toRGBA8 } from 'upng-js';
import { AtomStyleSpec, GLModel, LineStyleSpec } from "./GLModel";
import { ArrowSpec, BoxSpec, CurveSpec, CustomShapeSpec, CylinderSpec, GLShape, IsoSurfaceSpec, LineSpec, ShapeSpec, SphereSpec, splitMesh } from "./GLShape";
import { getGradient, Gradient } from "./Gradient";
import { Label, LabelSpec } from "./Label";
import { ProteinSurface, SurfaceType, syncSurface } from "./ProteinSurface4";
import { VolumeData } from "./VolumeData";
import { GLVolumetricRender, VolumetricRendererSpec } from "./VolumetricRender";
import { Camera, Coloring, Fog, FrontSide, Geometry, Light, Line, LineBasicMaterial, Material, Mesh, MeshDoubleLambertMaterial, MeshLambertMaterial, Object3D, Projector, Raycaster, Renderer, Scene } from "./WebGL";
import { Matrix3, Matrix4, Quaternion, Vector3, XYZ } from "./WebGL/math";
import { CC, ColorschemeSpec, ColorSpec, elementColors } from "./colors";
import { AtomSelectionSpec, AtomSpec } from "./specs";
import { adjustVolumeStyle, extend, getColorFromStyle, getElement, getExtent, getPropertyRange, isEmptyObject, makeFunction, mergeGeos, PausableTimer } from "./utilities";
export const CONTEXTS_PER_VIEWPORT = 16;
interface SurfObj {
geo: Geometry;
mat: Material;
done: Boolean;
finished: Boolean;
lastGL?: any;
symmetries?: any[];
style?: SurfaceStyleSpec;
}
/**
* A surface.
*
* @class
*/
class Surface extends Array<SurfObj> {
style?: SurfaceStyleSpec;
atomsel?: AtomSelectionSpec;
allsel?: AtomSelectionSpec;
focus?: AtomSelectionSpec;
constructor(items: SurfObj[]) {
super(...items); // spread the array elements into the Array constructor
}
/**
* Returns list of rotational/translational matrices if there is BIOMT data
* Otherwise returns a list of just the ID matrix
*
* @return {Array<Matrix4>}
*
*/
public getSymmetries() {
//we assume all sub-objects have same symmetries
if (this.length == 0) return [];
let obj = this[0];
if (typeof (obj.symmetries) == 'undefined') {
this.setSymmetries([new Matrix4()]);
}
return obj.symmetries;
};
/**
* Sets symmetries based on specified matrices in list
*
* @param {Array<Matrix4>} list
*
*/
public setSymmetries(syms) {
if (typeof (syms) == "undefined") { //delete sym data
syms = [new Matrix4()];
}
for (let obj of this) {
obj.symmetries = syms;
obj.finished = false; //trigger redraw
}
};
}
/**
* WebGL-based 3Dmol.js viewer
* Note: The preferred method of instantiating a GLViewer is through {@link createViewer}
*
* @class
*/
export class GLViewer {
// private class variables
private static numWorkers = 4; // number of threads for surface generation
private static maxVolume = 64000; // how much to break up surface calculations
private callback: any;
private defaultcolors: any;
private config: ViewerSpec;
private nomouse = false;
private bgColor: any;
private camerax: number;
private _viewer: GLViewer;
private glDOM: HTMLCanvasElement | null = null;
private models: GLModel[] = []; // atomistic molecular models
private surfaces: Record<number, Surface> = {};
private shapes = []; // Generic shapes
private labels: Label[] = [];
private clickables = []; //things you can click on
private hoverables = []; //things you can hover over
private contextMenuEnabledObjects = []; // atoms and shapes with context menu
private current_hover: any = null;
private hoverDuration = 500;
private longTouchDuration = 1000;
private viewer_frame = 0;
private WIDTH: number;
private HEIGHT: number;
private viewChangeCallback: any = null;
private stateChangeCallback: any = null;
private NEAR = 1;
private FAR = 800;
private CAMERA_Z = 150;
private fov = 20;
private linkedViewers = [];
private renderer: Renderer | null = null;
private row: number;
private col: number;
private cols: number;
private rows: number;
private viewers: any;
private control_all = false;
private ASPECT: any;
private camera: Camera;
private lookingAt: Vector3;
private raycaster: Raycaster;
private projector: Projector;
private scene: any = null;
private rotationGroup: any = null; // which contains modelGroup
private modelGroup: any = null;
private fogStart = 0.4;
private fogEnd = 1.0;
private slabNear = -50; // relative to the center of rotationGroup
private slabFar = 50;
public container: HTMLElement | null;
static readonly surfaceTypeMap = {
"VDW": SurfaceType.VDW,
"MS": SurfaceType.MS,
"SAS": SurfaceType.SAS,
"SES": SurfaceType.SES
};
private cq = new Quaternion(0, 0, 0, 1);
private dq = new Quaternion(0, 0, 0, 1);
private animated = 0;
private animationTimers = new Set<PausableTimer>();
private isDragging = false;
private mouseStartX = 0;
private mouseStartY = 0;
private touchDistanceStart = 0;
private touchHold = false;
private currentModelPos = 0;
private cz = 0;
private cslabNear = 0;
private cslabFar = 0;
private mouseButton: any;
private hoverTimeout: any;
private longTouchTimeout: any;
private divwatcher: any;
private intwatcher: any;
private spinInterval: any;
private getWidth() {
let div = this.container;
//offsetwidth accounts for scaling
let w = div.offsetWidth;
if (w == 0 && div.style.display === 'none') {
let oldpos = div.style.position;
let oldvis = div.style.visibility;
div.style.display = 'block';
div.style.visibility = 'hidden';
div.style.position = 'absolute';
w = div.offsetWidth;
div.style.display = 'none';
div.style.visibility = oldvis;
div.style.position = oldpos;
}
return w;
};
private getHeight() {
let div = this.container;
let h = div.offsetHeight;
if (h == 0 && div.style.display === 'none') {
let oldpos = div.style.position;
let oldvis = div.style.visibility;
div.style.display = 'block';
div.style.visibility = 'hidden';
div.style.position = 'absolute';
h = div.offsetHeight;
div.style.display = 'none';
div.style.visibility = oldvis;
div.style.position = oldpos;
}
return h;
};
private setupRenderer() {
let rendopt = {
...this.config,
preserveDrawingBuffer: true, //so we can export images
premultipliedAlpha: false,/* more traditional compositing with background */
//cannot initialize with zero size - render will start out lost
containerWidth: this.WIDTH,
containerHeight: this.HEIGHT
};
this.renderer = new Renderer(rendopt);
this.renderer.domElement.style.width = "100%";
this.renderer.domElement.style.height = "100%";
this.renderer.domElement.style.padding = "0";
this.renderer.domElement.style.position = "absolute"; //TODO: get rid of this
this.renderer.domElement.style.top = "0px";
this.renderer.domElement.style.left = "0px";
this.renderer.domElement.style.zIndex = "0";
}
private initializeScene() {
this.scene = new Scene();
this.scene.fog = new Fog(this.bgColor, 100, 200);
this.modelGroup = new Object3D();
this.rotationGroup = new Object3D();
this.rotationGroup.useQuaternion = true;
this.rotationGroup.quaternion = new Quaternion(0, 0, 0, 1);
this.rotationGroup.add(this.modelGroup);
this.scene.add(this.rotationGroup);
// setup lights
var directionalLight = new Light(0xFFFFFF);
directionalLight.position = new Vector3(0.2, 0.2, 1)
.normalize();
directionalLight.intensity = 1.0;
this.scene.add(directionalLight);
};
private _handleLostContext(event) {
//when contexts go missing, try to regenerate any that are visible on screen
//but no more than CONTEXTS_PER_VIEWPORT (if this is set higher than the
//browser limit there will be an infinity loop of refreshing contexts of
//too many are on screen)
const isVisible = function (cont) {
const rect = cont.getBoundingClientRect();
return !(
rect.right < 0 ||
rect.bottom < 0 ||
rect.top > (window.innerHeight || document.documentElement.clientHeight) ||
rect.left > (window.innerWidth || document.documentElement.clientWidth)
);
};
if (isVisible(this.container)) {
let restored = 0;
for (let c of document.getElementsByTagName('canvas')) {
if (isVisible(c) && (c as any)._3dmol_viewer != undefined) {
(c as any)._3dmol_viewer.resize();
restored += 1;
if (restored >= CONTEXTS_PER_VIEWPORT) break;
}
}
}
}
private initContainer(element) {
this.container = element;
this.WIDTH = this.getWidth();
this.HEIGHT = this.getHeight();
this.ASPECT = this.renderer.getAspect(this.WIDTH, this.HEIGHT);
this.renderer.setSize(this.WIDTH, this.HEIGHT);
this.container.append(this.renderer.domElement);
this.glDOM = this.renderer.domElement;
(this.glDOM as any)._3dmol_viewer = this;
this.glDOM.addEventListener("webglcontextlost", this._handleLostContext.bind(this));
if (!this.nomouse) {
// user can request that the mouse handlers not be installed
this.glDOM.addEventListener('mousedown', this._handleMouseDown.bind(this), { passive: false });
this.glDOM.addEventListener('touchstart', this._handleMouseDown.bind(this), { passive: false });
this.glDOM.addEventListener('wheel', this._handleMouseScroll.bind(this), { passive: false });
this.glDOM.addEventListener('mousemove', this._handleMouseMove.bind(this), { passive: false });
this.glDOM.addEventListener('touchmove', this._handleMouseMove.bind(this), { passive: false });
this.glDOM.addEventListener("contextmenu", this._handleContextMenu.bind(this), { passive: false });
}
};
private decAnim() {
//decrement the number of animations currently
this.animated--;
if (this.animated < 0) this.animated = 0;
};
private incAnim() {
this.animated++;
};
private nextSurfID() {
//compute the next highest surface id directly from surfaces
//this is necessary to support linking of model data
var max = 0;
for (let i in this.surfaces) { // this is an object with possible holes
if (!this.surfaces.hasOwnProperty(i)) continue;
var val = parseInt(i);
if (!isNaN(val)) {
if (val > max)
max = val;
}
}
return max + 1;
};
private setSlabAndFog() {
let center = this.camera.position.z - this.rotationGroup.position.z;
if (center < 1)
center = 1;
this.camera.near = center + this.slabNear;
if (!this.camera.ortho && this.camera.near < 1)
this.camera.near = 1;
this.camera.far = center + this.slabFar;
if (this.camera.near + 1 > this.camera.far)
this.camera.far = this.camera.near + 1;
this.camera.fov = this.fov;
this.camera.right = center * Math.tan(Math.PI / 180 * this.fov);
this.camera.left = -this.camera.right;
this.camera.top = this.camera.right / this.ASPECT;
this.camera.bottom = -this.camera.top;
this.camera.updateProjectionMatrix();
this.scene.fog.near = this.camera.near + this.fogStart * (this.camera.far - this.camera.near);
this.scene.fog.far = this.camera.near + this.fogEnd * (this.camera.far - this.camera.near);
if (this.config.disableFog) {
this.scene.fog.near = this.scene.fog.far;
}
};
// display scene
//if nolink is set/true, don't propagate changes to linked viewers
private show(nolink?) {
this.renderer.setViewport();
if (!this.scene)
return;
//let time = new Date();
this.setSlabAndFog();
this.renderer.render(this.scene, this.camera);
//console.log("rendered in " + (+new Date() - (time as any)) + "ms");
//have any scene change trigger a callback
if (this.viewChangeCallback) this.viewChangeCallback(this._viewer.getView());
if (!nolink && this.linkedViewers.length > 0) {
var view = this._viewer.getView();
for (var i = 0; i < this.linkedViewers.length; i++) {
var other = this.linkedViewers[i];
other.setView(view, true);
}
}
};
//regenerate the list of clickables
//also updates hoverables
private updateClickables() {
this.clickables.splice(0, this.clickables.length);
this.hoverables.splice(0, this.hoverables.length);
this.contextMenuEnabledObjects.splice(0, this.contextMenuEnabledObjects.length);
for (let i = 0, il = this.models.length; i < il; i++) {
let model = this.models[i];
if (model) {
let atoms = model.selectedAtoms({
clickable: true
});
let hoverable_atoms = model.selectedAtoms({
hoverable: true
});
let contextMenuEnabled_atom = model.selectedAtoms({ contextMenuEnabled: true });
// Array.prototype.push.apply(hoverables,hoverable_atoms);
for (let n = 0; n < hoverable_atoms.length; n++) {
this.hoverables.push(hoverable_atoms[n]);
}
// Array.prototype.push.apply(clickables, atoms); //add atoms into clickables
for (let m = 0; m < atoms.length; m++) {
this.clickables.push(atoms[m]);
}
// add atoms into contextMenuEnabledObjects
for (let m = 0; m < contextMenuEnabled_atom.length; m++) {
this.contextMenuEnabledObjects.push(contextMenuEnabled_atom[m]);
}
}
}
for (let i = 0, il = this.shapes.length; i < il; i++) {
let shape = this.shapes[i];
if (shape && shape.clickable) {
this.clickables.push(shape);
}
if (shape && shape.hoverable) {
this.hoverables.push(shape);
}
if (shape && shape.contextMenuEnabled) {
this.contextMenuEnabledObjects.push(shape);
}
}
};
// Checks for selection intersects on mousedown
private handleClickSelection(mouseX: number, mouseY: number, event) {
let intersects = this.targetedObjects(mouseX, mouseY, this.clickables);
// console.log('handleClickSelection', mouseX, mouseY, intersects);
if (intersects.length) {
var selected = intersects[0].clickable;
if (selected.callback !== undefined) {
if (typeof (selected.callback) != "function") {
selected.callback = makeFunction(selected.callback);
}
if (typeof (selected.callback) === "function") {
// Suppress click callbacks when context menu will be invoked.
// This only applies to clicks from "mouseup" events after right-click.
// Clicks from "touchend" after longtouch contextmenu are suppressed
// in _handleContextMenu.
const isContextMenu = this.mouseButton === 3
&& this.contextMenuEnabledObjects.includes(selected)
&& this.userContextMenuHandler;
if (!isContextMenu) {
selected.callback(selected, this._viewer, event, this.container, intersects);
}
}
}
}
};
//return offset of container
private canvasOffset() {
let canvas = this.glDOM;
let rect = canvas.getBoundingClientRect();
let doc = canvas.ownerDocument;
let docElem = doc.documentElement;
let win = doc.defaultView;
return {
top: rect.top + win.pageYOffset - docElem.clientTop,
left: rect.left + win.pageXOffset - docElem.clientLeft
};
};
//set current_hover to sel (which can be null), calling appropraite callbacks
private setHover(selected, event?, intersects?) {
if (this.current_hover == selected) return;
if (this.current_hover) {
if (typeof (this.current_hover.unhover_callback) != "function") {
this.current_hover.unhover_callback = makeFunction(this.current_hover.unhover_callback);
}
this.current_hover.unhover_callback(this.current_hover, this._viewer, event, this.container, intersects);
}
this.current_hover = selected;
if (selected && selected.hover_callback !== undefined) {
if (typeof (selected.hover_callback) != "function") {
selected.hover_callback = makeFunction(selected.hover_callback);
}
if (typeof (selected.hover_callback) === "function") {
selected.hover_callback(selected, this._viewer, event, this.container, intersects);
}
}
};
//checks for selection intersects on hover
private handleHoverSelection(mouseX, mouseY, event) {
if (this.hoverables.length == 0) return;
let intersects = this.targetedObjects(mouseX, mouseY, this.hoverables);
if (intersects.length) {
var selected = intersects[0].clickable;
this.setHover(selected, event, intersects);
this.current_hover = selected;
}
else {
this.setHover(null);
}
};
//sees if the mouse is still on the object that invoked a hover event and if not then the unhover callback is called
private handleHoverContinue(mouseX: number, mouseY: number) {
let intersects = this.targetedObjects(mouseX, mouseY, this.hoverables);
if (intersects.length == 0 || intersects[0] === undefined) {
this.setHover(null);
}
if (intersects[0] !== undefined && intersects[0].clickable !== this.current_hover) {
this.setHover(null);
}
};
/**
* Determine if a positioned event is "close enough" to mouseStart to be considered a click.
* With a mouse, the position should be exact, but allow a slight delta for a touch interface.
* @param {Event} event
* @param {{ allowTolerance, tolerance: number }} options
*/
private closeEnoughForClick(event, { allowTolerance = event.targetTouches, tolerance = 5 } = {}) {
const x = this.getX(event);
const y = this.getY(event);
if (allowTolerance) {
const deltaX = Math.abs(x - this.mouseStartX);
const deltaY = Math.abs(y - this.mouseStartY);
return deltaX <= tolerance && deltaY <= tolerance;
} else {
return x === this.mouseStartX && y === this.mouseStartY;
}
}
private calcTouchDistance(ev) { // distance between first two
// fingers
var xdiff = ev.targetTouches[0].pageX -
ev.targetTouches[1].pageX;
var ydiff = ev.targetTouches[0].pageY -
ev.targetTouches[1].pageY;
return Math.hypot(xdiff, ydiff);
};
//check targetTouches as well
private getX(ev) {
var x = ev.pageX;
if (x == undefined) x = ev.pageX; //firefox
if (ev.targetTouches &&
ev.targetTouches[0]) {
x = ev.targetTouches[0].pageX;
}
else if (ev.changedTouches &&
ev.changedTouches[0]) {
x = ev.changedTouches[0].pageX;
}
return x;
};
private getY(ev) {
var y = ev.pageY;
if (y == undefined) y = ev.pageY;
if (ev.targetTouches &&
ev.targetTouches[0]) {
y = ev.targetTouches[0].pageY;
}
else if (ev.changedTouches &&
ev.changedTouches[0]) {
y = ev.changedTouches[0].pageY;
}
return y;
};
//for grid viewers, return true if point is in this viewer
private isInViewer(x: number, y: number) {
if (this.viewers != undefined) {
var width = this.WIDTH / this.cols;
var height = this.HEIGHT / this.rows;
var offset = this.canvasOffset();
var relx = (x - offset.left);
var rely = (y - offset.top);
var r = this.rows - Math.floor(rely / height) - 1;
var c = Math.floor(relx / width);
if (r != this.row || c != this.col)
return false;
}
return true;
};
//if the user has specify zoom limits, readjust to fit within them
//also, make sure we don't go past CAMERA_Z
private adjustZoomToLimits(z: number) {
//a lower limit of 0 is at CAMERA_Z
if (this.config.lowerZoomLimit && this.config.lowerZoomLimit > 0) {
let lower = this.CAMERA_Z - this.config.lowerZoomLimit;
if (z > lower) z = lower;
}
if (this.config.upperZoomLimit && this.config.upperZoomLimit > 0) {
let upper = this.CAMERA_Z - this.config.upperZoomLimit;
if (z < upper) z = upper;
}
if (z > this.CAMERA_Z - 1) {
z = this.CAMERA_Z - 1; //avoid getting stuck
}
return z;
};
//interpolate between two normalized quaternions (t between 0 and 1)
//https://en.wikipedia.org/wiki/Slerp
private static slerp(v0: Quaternion, v1: Quaternion, t: number) {
// Compute the cosine of the angle between the two vectors.
//dot product
if (t == 1) return v1.clone();
else if (t == 0) return v0.clone();
let dot = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z + v0.w * v1.w;
if (dot > 0.9995) {
// If the inputs are too close for comfort, linearly interpolate
// and normalize the result.
let result = new Quaternion(
v0.x + t * (v1.x - v0.x),
v0.y + t * (v1.y - v0.y),
v0.z + t * (v1.z - v0.z),
v0.w + t * (v1.w - v0.w));
result.normalize();
return result;
}
// If the dot product is negative, the quaternions
// have opposite handed-ness and slerp won't take
// the shorted path. Fix by reversing one quaternion.
if (dot < 0.0) {
v1 = v1.clone().multiplyScalar(-1);
dot = -dot;
}
if (dot > 1) dot = 1.0;
else if (dot < -1) dot = -1.0;
var theta_0 = Math.acos(dot); // theta_0 = angle between input vectors
var theta = theta_0 * t; // theta = angle between v0 and result
var v2 = v1.clone();
v2.sub(v0.clone().multiplyScalar(dot));
v2.normalize(); // { v0, v2 } is now an orthonormal basis
var c = Math.cos(theta);
var s = Math.sin(theta);
var ret = new Quaternion(
v0.x * c + v2.x * s,
v0.y * c + v2.y * s,
v0.z * c + v2.z * s,
v0.w * c + v2.w * s
);
ret.normalize();
return ret;
};
/* @param {Object} element HTML element within which to create viewer
* @param {ViewerSpec} config Object containing optional configuration for the viewer
*/
constructor(element, c: ViewerSpec = {}) {
// set variables
this.config = c;
this.callback = this.config.callback;
this.defaultcolors = this.config.defaultcolors;
if (!this.defaultcolors)
this.defaultcolors = elementColors.defaultColors;
this.nomouse = Boolean(this.config.nomouse);
this.bgColor = 0;
this.config.backgroundColor = this.config.backgroundColor || "#ffffff";
if (typeof (this.config.backgroundColor) != 'undefined') {
this.bgColor = CC.color(this.config.backgroundColor).getHex();
}
this.config.backgroundAlpha = this.config.backgroundAlpha == undefined ? 1.0 : this.config.backgroundAlpha;
this.camerax = 0;
if (typeof (this.config.camerax) != 'undefined') {
this.camerax = typeof (this.config.camerax) === 'string' ? parseFloat(this.config.camerax) : this.config.camerax;
}
this._viewer = this;
this.container = element; //we expect container to be HTMLElement
if (this.config.hoverDuration != undefined) {
this.hoverDuration = this.config.hoverDuration;
}
if (this.config.antialias === undefined) this.config.antialias = true;
if (this.config.cartoonQuality === undefined) this.config.cartoonQuality = 10;
this.WIDTH = this.getWidth();
this.HEIGHT = this.getHeight();
this.setupRenderer();
this.row = this.config.row == undefined ? 0 : this.config.row;
this.col = this.config.col == undefined ? 0 : this.config.col;
this.cols = this.config.cols;
this.rows = this.config.rows;
this.viewers = this.config.viewers;
this.control_all = this.config.control_all;
this.ASPECT = this.renderer.getAspect(this.WIDTH, this.HEIGHT);
this.camera = new Camera(this.fov, this.ASPECT, this.NEAR, this.FAR, this.config.orthographic);
this.camera.position = new Vector3(this.camerax, 0, this.CAMERA_Z);
this.lookingAt = new Vector3();
this.camera.lookAt(this.lookingAt);
this.raycaster = new Raycaster(new Vector3(0, 0, 0), new Vector3(0, 0, 0));
this.projector = new Projector();
this.initializeScene();
this.renderer.setClearColorHex(this.bgColor, this.config.backgroundAlpha);
this.scene.fog.color = CC.color(this.bgColor);
// this event is bound to the body element, not the container,
// so no need to put it inside initContainer()
document.body.addEventListener('mouseup', this._handleMouseUp.bind(this));
document.body.addEventListener('touchend', this._handleMouseUp.bind(this));
this.initContainer(this.container);
if (this.config.style) { //enable setting style in constructor
this.setViewStyle(this.config as ViewStyle);
}
window.addEventListener("resize", this.resize.bind(this));
if (typeof (window.ResizeObserver) !== "undefined") {
this.divwatcher = new window.ResizeObserver(this.resize.bind(this));
this.divwatcher.observe(this.container);
}
if (typeof (window.IntersectionObserver) !== "undefined") {
//make sure a viewer that is becoming visible is alive
let intcallback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.resize();
}
});
};
this.intwatcher = new window.IntersectionObserver(intcallback);
this.intwatcher.observe(this.container);
}
try {
if (typeof (this.callback) === "function")
this.callback(this);
} catch (e) {
// errors in callback shouldn't invalidate the viewer
console.log("error with glviewer callback: " + e);
}
};
/**
* Return a list of objects that intersect that at the specified viewer position.
*
* @param x - x position in screen coordinates
* @param y - y position in screen coordinates
* @param {Object[]} - list of objects or selection object specifying what object to check for targeting
*/
public targetedObjects(x: number, y: number, objects) {
var mouse = {
x: x,
y: y,
z: -1.0
};
if (!Array.isArray(objects)) { //assume selection object
objects = this.selectedAtoms(objects);
}
if (objects.length == 0) return [];
this.raycaster.setFromCamera(mouse, this.camera);
return this.raycaster.intersectObjects(this.modelGroup, objects);
};
/** Convert model coordinates to screen coordinates.
* @param {object | list} - an object or list of objects with x,y,z attributes (e.g. an atom)
* @return {object | list} - and object or list of {x: screenX, y: screenY}
*/
public modelToScreen(coords) {
let returnsingle = false;
if (!Array.isArray(coords)) {
coords = [coords];
returnsingle = true;
}
let ratioX = this.renderer.getXRatio();
let ratioY = this.renderer.getYRatio();
let col = this.col;
let row = this.row;
let viewxoff = col * (this.WIDTH / ratioX);
//row is from bottom
let viewyoff = (ratioY - row - 1) * (this.HEIGHT / ratioY);
let results = [];
let offset = this.canvasOffset();
coords.forEach(coord => {
let t = new Vector3(coord.x, coord.y, coord.z);
t.applyMatrix4(this.modelGroup.matrixWorld);
this.projector.projectVector(t, this.camera);
let screenX = (this.WIDTH / ratioX) * (t.x + 1) / 2.0 + offset.left + viewxoff;
let screenY = -(this.HEIGHT / ratioY) * (t.y - 1) / 2.0 + offset.top + viewyoff;
results.push({ x: screenX, y: screenY });
});
if (returnsingle) results = results[0];
return results;
};
/**
* For a given screen (x,y) displacement return model displacement
* @param{x} x displacement in screen coordinates
* @param{y} y displacement in screen corodinates
* @param{modelz} z coordinate in model coordinates to compute offset for, default is model axis
*/
public screenOffsetToModel(x: number, y: number, modelz?) {
var dx = x / this.WIDTH;
var dy = y / this.HEIGHT;
var zpos = (modelz === undefined ? this.rotationGroup.position.z : modelz);
var q = this.rotationGroup.quaternion;
var t = new Vector3(0, 0, zpos);
this.projector.projectVector(t, this.camera);
t.x += dx * 2;
t.y -= dy * 2;
this.projector.unprojectVector(t, this.camera);
t.z = 0;
t.applyQuaternion(q);
return t;
};
/**
* Distance from screen coordinate to model coordinate assuming screen point
* is projected to the same depth as model coordinate
* @param{screen} xy screen coordinate
* @param{model} xyz model coordinate
*/
public screenToModelDistance(screen: XYZ, model) {
let offset = this.canvasOffset();
//convert model to screen to get screen z
let mvec = new Vector3(model.x, model.y, model.z);
mvec.applyMatrix4(this.modelGroup.matrixWorld);
let m = mvec.clone();
this.projector.projectVector(mvec, this.camera);
let t = new Vector3((screen.x - offset.left) * 2 / this.WIDTH - 1, (screen.y - offset.top) * 2 / -this.HEIGHT + 1, mvec.z);
this.projector.unprojectVector(t, this.camera);
return t.distanceTo(m);
};
/**
* Set a callback to call when the view has potentially changed.
*
*/
public setViewChangeCallback(callback) {
if (typeof (callback) === 'function' || callback == null)
this.viewChangeCallback = callback;
};
/**
* Set a callback to call when the view has potentially changed.
*
*/
public setStateChangeCallback(callback) {
if (typeof (callback) === 'function' || callback == null)
this.stateChangeCallback = callback;
};
/**
* Return configuration of viewer
*/
public getConfig() {
return this.config;
};
/**
* Set the configuration object. Note that some settings may only
* have an effect at viewer creation time.
*/
public setConfig(c: ViewerSpec) {
this.config = c;
if (c.ambientOcclusion) {
this.renderer.enableAmb