@ccp-nc/crystvis-js
Version:
A Three.js based crystallographic visualisation tool
1,601 lines (1,374 loc) • 70.2 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"><title>Source: model.js</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"><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="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="lib_model.module_js.html">lib/model.js</a></div><div class="sidebar-section-children"><a href="lib_modelview.module_js.html">lib/modelview.js</a></div><div class="sidebar-section-children"><a href="lib_visualizer.module_js.html">lib/visualizer.js</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><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="lib_model.module_js-AtomImage.html">AtomImage</a></div><div class="sidebar-section-children"><a href="lib_model.module_js-BondImage.html">BondImage</a></div><div class="sidebar-section-children"><a href="lib_model.module_js-Model.html">Model</a></div><div class="sidebar-section-children"><a href="lib_modelview.module_js-ModelView.html">ModelView</a></div><div class="sidebar-section-children"><a href="lib_visualizer.module_js-CrystVis.html">CrystVis</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-tutorials"><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-Events.html">Events</a></div><div class="sidebar-section-children"><a href="tutorial-Queries.html">Queries</a></div><div class="sidebar-section-children"><a href="tutorial-ThreejsMigration.html">ThreejsMigration</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></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="#light-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">model.js</h1></header><article><pre class="prettyprint source lang-js"><code>'use strict';
/**
* @fileoverview Class holding the atomic models to be plotted
* @module
*/
import _ from 'lodash';
import * as mjs from 'mathjs';
import {
PeriodicTable as PeriodicTable
} from 'mendeleev';
import {
Atoms as Atoms
} from '@ccp-nc/crystcif-parse';
import * as utils from './utils.js';
import * as data from './data.js';
import {
QueryParser as QueryParser
} from './query.js';
import {
ModelView as ModelView
} from './modelview.js';
const LABEL_HEIGHT = 0.04; // For now fixed, just a value that works
/** An 'image' of a single atom from a model. This represents a specific periodic copy of that atom (if applicable). */
class AtomImage {
/**
* @class
* @param {Model} model The model from which the image is from
* @param {int} index Index of the atom in the model
* @param {Array} ijk Indices of the cell in which the image is located
*/
constructor(model, index, ijk) {
this._model = model;
this._index = index;
this._ijk = ijk || [0, 0, 0];
// String ID
this._id = this._index + '_' + _.join(this._ijk, '_');
// Integer index
this._img_index = utils.supercellIndex(index, this._ijk,
model.supercell, model.length);
this._xyz0 = model._positions[index];
this._bondsFrom = []; // BondImages of bonds for which this is atom1
this._bondsTo = []; // BondImages of bonds for which this is atom2
if (!model.periodic) {
this._fxyz0 = null;
this._fxyz = null;
this._xyz = this._xyz0;
} else {
this._fxyz0 = model._scaled_positions[index];
this._fxyz = [this._fxyz0[0] + ijk[0],
this._fxyz0[1] + ijk[1],
this._fxyz0[2] + ijk[2]
];
this._xyz = mjs.multiply(this._fxyz, model._cell);
}
this._isotope = null; // By default look up the model
// Visual properties
this._visible = false;
this._color = this.cpkColor;
this._uses_cpk = true;
this._base_radius = this.vdwRadius / 4.0;
this._scale = 1.0;
this._opacity = 1.0;
this._highlighted = false;
this._mesh = null; // Will be created when first requested
this._aura = null;
this._labels = {};
this._ellipsoids = {};
}
/**
* Model this atom belongs to
* @readonly
* @type {Model}
*/
get model() {
return this._model;
}
/**
* Renderer used by this atom
* @readonly
* @type {Renderer}
*/
get renderer() {
var m = this.model;
if (m) {
return m._renderer;
}
return null;
}
/**
* Index of the atom
* @readonly
* @type {int}
*/
get index() {
return this._index;
}
/**
* String ID of the image
* @readonly
* @type {String}
*/
get id() {
return this._id;
}
/**
* Index of this image
* @readonly
* @type {int}
*/
get imgIndex() {
return this._img_index;
}
/**
* Index of the species of this atom
* @readonly
* @type {int}
*/
get speciesIndex() {
return this._model._species_indices[this._index];
}
/**
* Symbol of this atom's element
* @readonly
* @type {String}
*/
get element() {
return this._model._elems[this._index];
}
/**
* Crystal site label of this atom
* @readonly
* @type {String}
*/
get crystLabel() {
return this._model._labels[this._index];
}
/**
* Periodic table information for this atom's element
* @readonly
* @type {Object}
*/
get elementData() {
return data.getElementData(this.element);
}
/**
* Information for this atom's isotope
* @readonly
* @type {Object}
*/
get isotopeData() {
let idata = this._isotope;
if (idata === null)
idata = this._model._isotopes[this._index];
return idata;
}
/**
* Atomic mass of this atom's isotope
* @type {int}
*/
get isotope() {
return this.isotopeData.A;
}
set isotope(A) {
this._isotope = data.getIsotopeData(this.element, A);
if (this._isotope === null)
throw Error('Isotope does not exist for this element');
// Reset color
if (this._uses_cpk) {
this.color = null;
}
}
/**
* Atomic mass of the global isotope set as default for this atom's species
* @type {int}
*/
set isotopeGlobal(A) {
// Set the isotope for this atom in the model
let iso = data.getIsotopeData(this.element, A);
if (iso === null)
throw Error('Isotope does not exist for this element');
this._model._isotopes[this._index] = iso;
// Reset color
if (this._uses_cpk) {
this.color = null;
}
}
/**
* Atomic number of element
* @readonly
* @type {int}
*/
get number() {
var el = PeriodicTable.getElement(this.element);
return (el ? el.number : 0);
}
/**
* Hex integer code of the conventional CPK color used for this element
* (altered in case of non-standard isotopes)
* @readonly
* @type {int}
*/
get cpkColor() {
return data.getCpkColor(this.element, this.isotope);
}
/**
* Van dew Waals radius for this element
* @readonly
* @type {float}
*/
get vdwRadius() {
return data.getVdwRadius(this.element);
}
/**
* Bonds from this atom
* @readonly
* @type {BondImage[]}
*/
get bondsFrom() {
return Array.from(this._bondsFrom);
}
/**
* Bonds to this atom
* @readonly
* @type {BondImage[]}
*/
get bondsTo() {
return Array.from(this._bondsTo);
}
/**
* All bonds connected to this atom
* @readonly
* @type {BondImage[]}
*/
get bonds() {
return _.concat(this._bondsFrom, this._bondsTo);
}
/**
* All atoms bonded to this atom
* @readonly
* @type {AtomImage[]}
*/
get bondedAtoms() {
return _.concat(_.map(this._bondsFrom, function(b) {
return b.atom2;
}),
_.map(this._bondsTo, function(b) {
return b.atom1;
}));
}
/**
* Cell indices of this atom image
* @readonly
* @type {int[]}
*/
get ijk() {
return Array.from(this._ijk);
}
/**
* Position of this atom's original
* @readonly
* @type {float[]}
*/
get xyz0() {
return Array.from(this._xyz0);
}
/**
* Position of this atom image
* @readonly
* @type {float[]}
*/
get xyz() {
return Array.from(this._xyz);
}
/**
* Fractional coordinates of this atom's original
* @readonly
* @type {float[]}
*/
get fxyz0() {
return Array.from(this._fxyz0);
}
/**
* Fractional coordinates of this atom image
* @readonly
* @type {float[]}
*/
get fxyz() {
return Array.from(this._fxyz);
}
/**
* Index of the molecule this atom belongs to
* @readonly
* @type {int}
*/
get moleculeIndex() {
return this._model._molinds[this._index];
}
/**
* Mesh corresponding to this atom image
* @readonly
* @type {AtomMesh}
*/
get mesh() {
var r = this.renderer;
if (!this._mesh && r) {
this._mesh = new r.Primitives.AtomMesh(this._xyz, this.radius, this._color);
this._mesh.image = this;
}
return this._mesh;
}
/**
* Aura used to highlight this atom image
* @readonly
* @type {AuraMesh}
*/
get aura() {
var r = this.renderer;
if (!this._aura && r) {
this._aura = new r.Primitives.AuraMesh({
radius: this.radius,
scale: 0.02
});
this.mesh.add(this._aura);
}
return this._aura;
}
// Get and set graphical properties
/**
* Whether the atom is visible
* @type {bool}
*/
get visible() {
return this._visible;
}
set visible(v) {
this._visible = v;
var mesh = this.mesh;
if (v) {
this.renderer.add(mesh, 'model');
} else {
this.renderer.remove(mesh, 'model');
}
// Update aura visibility
this.highlighted = this._highlighted;
// Update connected bonds' visibility
for (let i = 0; i < this._bondsFrom.length; ++i) {
let b = this._bondsFrom[i];
b.visible = b._visible;
}
for (let i = 0; i < this._bondsTo.length; ++i) {
let b = this._bondsTo[i];
b.visible = b._visible;
}
}
/**
* Starting radius of the atom
* @type {float}
*/
get baseRadius() {
return this._base_radius;
}
set baseRadius(r) {
if (r == null) { // Default value
r = this.vdwRadius / 4.0;
}
this._base_radius = r;
var mesh = this.mesh;
mesh.atom_radius = this.radius;
}
/**
* Scale of the atom
* @type {float}
*/
get scale() {
return this._scale;
}
set scale(s) {
if (s == null) {
s = 1;
}
this._scale = s;
var mesh = this.mesh;
mesh.atom_radius = this.radius;
}
/**
* Final radius of the atom (starting radius * scale)
* @type {float}
*/
get radius() {
return this._scale * this._base_radius;
}
set radius(r) {
if (r == null) {
r == this.baseRadius;
}
this.scale = r / this._base_radius;
}
/**
* Color of the atom
* @type {int}
*/
get color() {
return this._color;
}
set color(c) {
if (c === null) {
c = this.cpkColor;
this._uses_cpk = true;
}
else {
this._uses_cpk = false;
}
this._color = c;
var mesh = this.mesh;
if (mesh) {
mesh.atom_color = c;
}
_.map(this._bondsFrom, function(b) {
b.color1 = c;
});
_.map(this._bondsTo, function(b) {
b.color2 = c;
});
}
/**
* Opacity of the atom
* @type {float}
*/
get opacity() {
return this._opacity;
}
set opacity(o) {
if (o == null) {
o = 1;
}
this._opacity = o;
var mesh = this.mesh;
mesh.atom_opacity = o;
_.map(this._bondsFrom, function(b) {
b.opacity1 = o;
});
_.map(this._bondsTo, function(b) {
b.opacity2 = o;
});
}
/**
* Whether the atom is highlighted
* @type {bool}
*/
get highlighted() {
return this._highlighted;
}
set highlighted(h) {
if (h == null) {
h = false;
}
this._highlighted = h;
var aura = this.aura;
if (h && this._visible) {
aura.visible = true;
} else {
aura.visible = false;
}
}
/**
* Add a text label to the atom.
*
* @param {String} text Content of the label
* @param {String} name Name to use to refer to the label (necessary to overwrite/erase later)
* @param {Object} parameters Dictionary of other options (e.g. font family, text color, etc. See TextSprite)
*/
addLabel(text, name, parameters = {}) {
this.removeLabel(name); // Precautionary
var defaults = {
faceCamera: true,
fixScale: true,
shift: [1.0*this.radius, 0, 0], // This just works well
height: LABEL_HEIGHT,
};
parameters = _.merge(defaults, parameters);
parameters.position = [0, 0, 0]; // This is not customizable
var r = this.renderer;
if (r) {
var label = new r.Primitives.TextSprite(text, parameters);
this._labels[name] = label;
this.mesh.add(label);
}
}
/**
* Remove the label of a given name
*
* @param {String} name Name of the label
*/
removeLabel(name) {
let l = this._labels[name];
if (l)
this._mesh.remove(l);
delete this._labels[name];
}
/**
* Retrieve or set a label's properties
*
* @param {String} name Name of the label
* @param {String} property Property to set
* @param {?} value Value to set. If omitted, returns the current
* value instead.
*/
labelProperty(name, property, value = null) {
if (value) {
this._labels[name][property] = value;
} else {
return this._labels[name][property];
}
}
/**
* Add an ellipsoid to the atom.
*
* @param {TensorData | Object | Array} data The data to base the
* ellipsoid on. Can be:
* - a TensorData object;
* - an Object with 'eigenvalues'
* and 'eigenvectors' members
* - an Array of the form
* [eigenvalues, eigenvectors]
* @param {String} name Name of the ellipsoid
* @param {Object} parameters Additional options to
* pass (see EllipsoidMesh)
*/
addEllipsoid(data, name, parameters = {}) {
this.removeEllipsoid(name);
parameters = _.clone(parameters); // Avoid editing the reference object
if (data instanceof Array) {
parameters.eigenvalues = data[0];
parameters.eigenvectors = data[1];
} else {
parameters.eigenvalues = data.eigenvalues;
parameters.eigenvectors = data.eigenvectors;
}
if (parameters.ditherSeed == null) {
// As long as it's consistent for a given atom, the actual value is irrelevant
let seed = utils.hashCode(this._fxyz + name);
parameters.ditherSeed = seed/4294967295.0; // Reduce to ]0.5,-0.5]
}
else {
}
var r = this.renderer;
if (r) {
var ellips = new r.Primitives.EllipsoidMesh(parameters);
this._ellipsoids[name] = ellips;
this.mesh.add(ellips);
}
}
/**
* Remove the ellipsoid with a given name
*
* @param {String} name Name of the ellipsoid
*/
removeEllipsoid(name) {
let l = this._ellipsoids[name];
if (l)
this._mesh.remove(l);
delete this._ellipsoids[name];
}
/**
* Retrieve or set an ellipsoid's properties
*
* @param {String} name Name of the ellipsoid
* @param {String} property Property to set
* @param {?} value Value to set. If omitted, returns the current
* value instead.
*/
ellipsoidProperty(name, property, value = null) {
if (value) {
this._ellipsoids[name][property] = value;
} else {
return this._ellipsoids[name][property];
}
}
/**
* Get the value for one array for this image
* @param {String} name Name of the array
*
* @return {*} Value of the array for this atom
*/
getArrayValue(name) {
return this._model.getArray(name)[this._index];
}
// Check equality with another image
equals(ai) {
return (this._model == ai._model &&
this._index == ai._index &&
_.isEqual(this._ijk, ai._ijk));
}
// Return a copy, possibly shifted to a different cell
copy(shift = [0, 0, 0]) {
return new AtomImage(this._model,
this._index,
mjs.add(this._ijk, shift));
}
}
/** An 'image' of a single bond in the model. This represents the connection
between two specific AtomImages */
class BondImage {
/**
* @class
* @param {Model} model The model from which the image is from
* @param {AtomImage} im1 AtomImage from which the bond starts
* @param {AtomImage} im2 AtomImage to which the bond ends
*/
constructor(model, im1, im2) {
this._model = model;
this._im1 = im1;
this._im2 = im2;
this._im1._bondsFrom.push(this);
this._im2._bondsTo.push(this);
this._length = mjs.distance(this._im1.xyz, this._im2.xyz);
this._key = this._im1.imgIndex + '_' + this._im2.imgIndex;
// Visual properties
this._visible = true;
this._radius = 0.2;
this._opacity = 1.0;
this._mesh = null; // Created on first request
}
/**
* Model this bond belongs to
* @readonly
* @type {Model}
*/
get model() {
return this._model;
}
/**
* Renderer used by this bond
* @readonly
* @type {Renderer}
*/
get renderer() {
var m = this.model;
if (m) {
return m._renderer;
}
return null;
}
/**
* First atom connected to this bond
* @readonly
* @type {AtomImage}
*/
get atom1() {
return this._im1;
}
/**
* Second atom connected to this bond
* @readonly
* @type {AtomImage}
*/
get atom2() {
return this._im2;
}
/**
* A unique string key used to quickly reference the bond
* @readonly
* @type {String}
*/
get key() {
// Used in dictionary for quick reference
return this._key;
}
/**
* Bond length in Angstroms
* @readonly
* @type {float}
*/
get length() {
return this._length;
}
/**
* Mesh corresponding to this bond image
* @readonly
* @type {AtomMesh}
*/
get mesh() {
var r = this.renderer;
if (!this._mesh && r) {
this._mesh = new r.Primitives.BondMesh(this.atom1.xyz, this.atom2.xyz,
this._radius,
this.atom1.color, this.atom2.color);
}
return this._mesh;
}
/**
* Radius of the bond
* @type {float}
*/
get radius() {
return this._radius;
}
set radius(r) {
if (r == null) {
r = 0.2;
}
this._radius = r;
var mesh = this.mesh;
if (mesh) {
mesh.bond_radius = r;
}
}
/**
* First color of the bond
* @type {int}
*/
set color1(c) {
if (c == null) {
c = this._im1.color;
}
var mesh = this.mesh;
if (mesh) {
mesh.bond_color_1 = c;
}
}
/**
* Second color of the bond
* @type {int}
*/
set color2(c) {
if (c == null) {
c = this._im2.color;
}
var mesh = this.mesh;
if (mesh) {
mesh.bond_color_2 = c;
}
}
/**
* First opacity of the bond
* @type {float}
*/
set opacity1(o) {
if (o == null) {
o = this._im1.opacity;
}
var mesh = this.mesh;
if (mesh) {
mesh.bond_opacity_1 = o;
}
}
/**
* Second opacity of the bond
* @type {float}
*/
set opacity2(o) {
if (o == null) {
o = this._im2.opacity;
}
var mesh = this.mesh;
if (mesh) {
mesh.bond_opacity_2 = o;
}
}
/**
* Whether the bond is visible
* @type {bool}
*/
get visible() {
return this._visible;
}
set visible(v) {
this._visible = v;
v = v && this.atom1.visible && this.atom2.visible;
var mesh = this.mesh;
if (v) {
this.renderer.add(mesh, 'model');
} else {
this.renderer.remove(mesh, 'model');
}
}
}
class Model {
/**
* An object containing an Atomic structure and taking care of its periodic
* nature, allowing querying and selection, and so on.
* @class
* @param {crystcif.Atoms} atoms Atomic structure, in crystcif's Atoms format
* @param {Object} parameters Additional options:
*
* - `supercell`
* - `molecularCrystal` (if true, load full molecules in central unit cell)
* - `useNMRActiveIsotopes` (if true, all isotopes are set by default to the most common
* one with non-zero spin)
* - `vdwScaling` (scale van der Waals radii by a constant factor)
* - `vdwElementScaling` (table of per-element factors to scale VdW radii by)
*/
constructor(atoms, parameters = {}) {
var defaults = {
supercell: [1, 1, 1],
molecularCrystal: false,
useNMRActiveIsotopes: false,
vdwScaling: 1.0,
vdwElementScaling: {}
};
parameters = _.merge(defaults, parameters);
this._vdwScaling = parameters.vdwScaling;
this._vdwElementScaling = parameters.vdwElementScaling;
const initMolecules = ((atoms, supercell) => {
if (!(atoms instanceof Atoms)) {
throw new Error('Model must be initialised with a loaded Atoms object');
}
this._atoms_base = atoms;
this._data = {};
/* Load the positions, cell, and other key data
Important: to save memory, we're simply storing references.
These are NOT to be changed!
*/
this._elems = this._atoms_base._arrays['symbols'];
this._isotopes = this._elems.map((el) => {
const iso = parameters.useNMRActiveIsotopes? 'nmr' : null;
let isodata = data.getIsotopeData(el, iso);
if (isodata === null) {
// No NMR active isotope?
isodata = data.getIsotopeData(el);
}
return isodata;
});
this._nums = this._atoms_base._arrays['numbers'];
this._positions = this._atoms_base._arrays['positions'];
this._cell = this._atoms_base._cell;
this._pbc = this._atoms_base._pbc;
this._periodic = !this._pbc.includes(false);
this._inv_cell = this._atoms_base._inv_cell;
this._supercell = [1, 1, 1];
this._supercell_grid = [
[0, 0, 0]
];
// Species indices (used for labels)
let sp_count = {};
this._species_indices = [];
this._species_indices = this._elems.map((s, i) => {
let c = sp_count[s];
c = c? c : 0;
sp_count[s] = c+1;
return c;
});
let has_cif_labels = false;
// Crystallographic labels
if ('labels' in this._atoms_base._arrays) {
// If any of the labels don't match the element,
// then we're assuming they're crystallographic (CIF-style) labels
if (this._atoms_base._arrays['labels'].some((l, i) => {
return l !== this._elems[i];
})) {
// then use them
has_cif_labels = true;
this._labels = this._atoms_base._arrays['labels'];
} else {
// otherwise, build new ones and
// throw a warning to user syaing we're doing this
this._labels = [];
for (let i = 0; i < this._elems.length; ++i) {
this._labels.push(this._elems[i] + '_' + (this._species_indices[i]+1));
}
console.warn('No crystallographic labels found in CIF file. Building new ones.');
}
}
else {
// Build them
this._labels = [];
for (let i = 0; i < this._elems.length; ++i) {
this._labels.push(this._elems[i] + '_' + (this._species_indices[i]+1));
}
}
this._has_cif_labels = has_cif_labels; // defaults to false
// Cryst label indices
let lab_count = {};
this._label_indices = [];
this._label_indices = this._labels.map((s, i) => {
let c = lab_count[s];
c = c? c : 0;
lab_count[s] = c+1;
return c;
});
if (this._periodic) {
// R matrix: indispensable for calculations of periodic distances
this._r_matrix = mjs.multiply(this._cell, mjs.transpose(this._cell));
var ediag = mjs.eigs(this._r_matrix);
// Sort by eigenvalue
var evecs = ediag.eigenvectors.map(e => e.vector);
ediag = _.zip(ediag.values, evecs);
ediag = _.sortBy(ediag, function(x) {
return x[0];
});
ediag = _.unzip(ediag);
this._r_diag = {
values: ediag[0],
vectors: ediag[1],
};
this._supercell = supercell; // Default
this._supercell_grid = utils.supercellGrid(supercell);
this._scaled_positions = this._atoms_base.get_scaled_positions();
}
// Compile all images for this supercell
this._atom_images = this._atomImages();
this._computeBonds();
this._computeMolecules();
}).bind(this);
initMolecules(atoms, parameters.supercell);
// if parameters.molecularCrystal, is null, we need to check if the atoms
// contains organic molecules -- i.e. if there is at least one C-H bond
if (parameters.molecularCrystal || (parameters.molecularCrystal === null &&
this._queryCHBond())) {
this._molecularCrystal = true;
atoms = _.cloneDeep(atoms);
var pos = this.positions;
for (let i = 0; i < this.length; ++i) {
let mol_i = this._molinds[i];
let mol = this._molecules[mol_i];
for (let j = 0; j < mol.length; ++j) {
var a = mol[j];
if (a.index == i) {
pos[i] = mjs.add(pos[i], this.fracToAbs(a.cell));
}
}
}
atoms.set_array('positions', pos);
initMolecules(atoms, parameters.supercell);
}
this._primitives = {}; // Any additional primitives drawn on this model
this._bond_images = this._bondImages();
// A special ModelView for convenience
this._all = new ModelView(this, _.range(this._atom_images.length));
// Parser for queries
this._qparse = new QueryParser({
'all': this._queryAll,
'indices': this._queryIndices,
'elements': this._queryElements,
'cell': this._queryCell,
'box': this._queryBox,
'sphere': this._querySphere,
'bonded': this._queryBonded,
'molecule': this._queryMolecule,
}, this);
// By default no rendering
this.renderer = null;
}
// Using the .get_ methods of _atoms guarantees these are copies,
// not pointers to the real thing
/**
* Number of atoms in this model's original cell
* @readonly
* @type {int}
*/
get length() {
return this._atoms_base.length();
}
/**
* Chemical symbols in this model's original cell
* @readonly
* @type {String[]}
*/
get symbols() {
return this._atoms_base.get_chemical_symbols();
}
/**
* Atomic numbers in this model's original cell
* @readonly
* @type {int[]}
*/
get numbers() {
return this._atoms_base.get_atomic_numbers();
}
/**
* Coordinates of the atoms in this model's original cell
* @readonly
* @type {Array[]}
*/
get positions() {
return this._atoms_base.get_positions();
}
/**
* Fractional coordinates of the atoms in this model's original cell
* @readonly
* @type {Array[]}
*/
get scaledPositions() {
return this._atoms_base.get_scaled_positions();
}
/**
* Unit cell of the model's original cell
* @readonly
* @type {Array[]}
*/
get cell() {
return this._atoms_base.get_cell();
}
/**
* Periodic boundary conditions
* @readonly
* @type {bool[]}
*/
get pbc() {
return this._atoms_base.get_pbc();
}
/**
* Additional information from the model's original cell
* @readonly
* @type {Object}
*/
get info() {
return this._atoms_base.info;
}
/**
* Whether this model is periodic in all three directions of space
* @readonly
* @type {bool}
*/
get periodic() {
return this._periodic;
}
/**
* Indices of each atom by their species (e.g. C1, C2, H1, C3, H2, etc.)
* @readonly
* @type {int[]}
*/
get speciesIndices() {
return Array.from(this._species_indices);
}
/**
* Crystallographic labels of each atom
* @readonly
* @type {String[]}
*/
get crystalLabels() {
return Array.from(this._labels);
}
/**
* Shape of the supercell for this model
* @readonly
* @type {int[]}
*/
get supercell() {
return Array.from(this._supercell);
}
/**
* Full grid of origin coordinates of the cells making up the supercell
* @readonly
* @type {Array[]}
*/
get supercellGrid() {
return JSON.parse(JSON.stringify(this._supercell_grid));
}
/**
* Atom images in this model
* @readonly
* @type {AtomImage[]}
*/
get atoms() {
return Array.from(this._atom_images);
}
/**
* ModelView containing all the atoms of the image
* @readonly
* @type {ModelView}
*/
get all() {
return this._all;
}
/**
* Graphical object representing the unit cell's axes
* @readonly
* @type {AxesMesh}
*/
get axes() {
return this._cartesian_axes;
}
/**
* Graphical object representing the unit cell's box
* @readonly
* @type {BoxMesh}
*/
get box() {
return this._cartesian_box;
}
/**
* Global scaling factor for Van der Waals radii
* @readonly
* @type {float}
*/
get vdwScaling() {
return this._vdwScaling;
}
/**
* Table of scaling factors by element for Van der Waals radii
* @readonly
* @type {Object}
*/
get vdwElementScaling() {
return JSON.parse(JSON.stringify(this._vdwElementScaling));
}
/**
* Renderer used for this model's graphics
* @type {Renderer}
*/
set renderer(r) {
if (r) {
this._renderer = r;
if (this.periodic) {
// Create axes and box
if (!this._cartesian_box) {
this._cartesian_box = new r.Primitives.BoxMesh(this.cell, {color:r.theme.cell_line_color});
}
if (!this._cartesian_axes) {
this._cartesian_axes = new r.Primitives.AxesMesh(this.cell, {
linewidth: 1.5
});
}
r.add(this._cartesian_box);
r.add(this._cartesian_axes);
}
// And the primitives
for (var name in this._primitives) {
var p = this._primitives[name];
r.add(p);
}
} else {
if (this._renderer)
this._renderer.clear();
this._renderer = null;
}
}
// Set and get arrays on the underlying Atoms object
/**
* Set an array for the underlying Atoms object
* @param {String} name Name of the array to use
* @param {Array} arr Array to store
*/
setArray(name, arr) {
this._atoms_base.set_array(name, arr);
}
/**
* Retrieve an array from the underlying Atoms object
* @param {String} name Name of the array to retrieve
* @return {Array} Retrieved array
*/
getArray(name) {
return this._atoms_base.get_array(name);
}
/**
* Check if an array exists in the underlying Atoms object
* @param {String} name Name of the array to check
* @return {bool} Whether the array exists
*/
hasArray(name) {
return (name in this._atoms_base._arrays);
}
/**
* Delete an array from the underlying Atoms object
* @param {String} name Name of the array to delete
*/
deleteArray(name) {
delete this._atoms_base._arrays[name];
}
// These functions are for adding and removing graphical representations
// that are meant to be drawn on to of the existing 3D model
/**
* Add link drawn on model
*
* @param {Atom | Array} from Starting point
* @param {Atom | Array} to End point
* @param {String} name Name to use for the link object
* @param {String} label Text label to add to the link
* @param {Object} parameters Additional parameters (see LineMesh)
*/
addLink(from, to, name = 'link', label = null, parameters = {}) {
this.removeGraphics(name);
parameters = _.clone(parameters); // Avoid editing the reference object
var r = this._renderer;
if (r) {
var link = new r.Primitives.LineMesh(from, to, parameters);
this._primitives[name] = link;
r.add(link);
if (label) {
var text = new r.Primitives.TextSprite(label, {
color: parameters.color,
fixScale: true,
faceCamera: true,
height: parameters.height || LABEL_HEIGHT,
shift: [LABEL_HEIGHT, 0, 0],
onOverlay: parameters.onOverlay
});
link.add(text);
}
}
}
/**
* Add a sphere drawn on model
*
* @param {Atom | Array} center Center of the sphere
* @param {float} radius Radius of the sphere
* @param {String} name Name to use for the sphere object
* @param {Object} parameters Additional parameters (see EllipsoidMesh)
*/
addSphere(center, radius, name='sphere', parameters = {}) {
this.removeGraphics(name);
var r = this._renderer;
if (r) {
parameters = _.merge({
color: 0xffffff,
opacity: 0.5,
opacityMode: r.Primitives.EllipsoidMesh.DITHER,
showCircles: true,
showAxes: true
}, parameters); // Avoid editing the reference object
var sph = new r.Primitives.EllipsoidMesh({
color: parameters.color,
opacity: parameters.opacity,
opacityMode: parameters.opacityMode,
showCircles: parameters.showCircles,
showAxes: parameters.showAxes,
scalingFactor: radius,
center: center
});
this._primitives[name] = sph;
r.add(sph);
}
}
/**
* Remove the graphical object with a given name
*
* @param {String} name Name of the graphical object to remove
*/
removeGraphics(name) {
var g = this._primitives[name];
var r = this._renderer;
if (g && r)
r.remove(g);
delete this._primitives[name];
}
/**
* Remove all graphical objects
*/
clearGraphics() {
var r = this._renderer;
if (r) {
_.map(this._primitives, function(g) {
r.remove(g);
});
}
this._primitives = {};
}
/**
* Compute the bonds within the model. For internal use
* @private
*/
_computeBonds() {
var N = this.length;
this._bondmat = Array(N); // Bond matrix
this._bondmat = _.map(this._bondmat, function() {
return _.map(Array(N), function() {
return [];
});
});
// Van der Waals radii by element
var vdwf = this._vdwScaling;
var vdwf_table = this._vdwElementScaling;
var vdwr = _.map(this.symbols, function(s) {
var f = vdwf;
if (s in vdwf_table) {
f = vdwf_table[s];
}
return data.getVdwRadius(s)*f;
});
var maxr = _.max(vdwr);
var cell = this.cell;
var sgrid = [
[0, 0, 0]
];
var p = this._positions;
if (this._periodic) {
var scell = this.minimumSupercell(maxr);
sgrid = utils.supercellGrid(scell);
}
// Now iterate over all atom pairs
for (let i = 0; i < this.length; ++i) {
var p1 = p[i];
for (let j = i; j < this.length; ++j) {
var p2 = p[j];
for (let k = 0; k < sgrid.length; ++k) {
var c = sgrid[k];
if ((i == j) && (c[0] == 0 && c[1] == 0 && c[2] == 0)) {
// Just the same atom, skip
continue;
}
var r = [0, 0, 0];
// Here we write the algebra explicitly
// for efficiency reasons
if (this._periodic) {
r[0] = c[0] * cell[0][0] + c[1] * cell[1][0] + c[2] * cell[2][0];
r[1] = c[0] * cell[0][1] + c[1] * cell[1][1] + c[2] * cell[2][1];
r[2] = c[0] * cell[0][2] + c[1] * cell[1][2] + c[2] * cell[2][2];
}
r = [p2[0] - p1[0] + r[0], p2[1] - p1[1] + r[1], p2[2] - p1[2] + r[2]];
r = Math.sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2]);
if (r < (vdwr[i] + vdwr[j]) / 2.0) {
// Bond!
this._bondmat[i][j].push([c[0], c[1], c[2]]);
this._bondmat[j][i].push([-c[0], -c[1], -c[2]]);
}
}
}
}
}
/**
* Check if any C-H bonds are present
* @return {bool} Whether any C-H bonds are present
* @private
*/
_queryCHBond() {
// make sure bondmat is present
if (!this._bondmat) {
this._computeBonds();
}
var symbols = this._atoms_base.get_chemical_symbols();
var bondmat = this._bondmat;
var n = symbols.length;
for (var i = 0; i < n; i++) {
var bonds = bondmat[i];
var a = symbols[i];
if (a == 'C') {
// loop over bonds and check if any are H
for (var j = 0; j < n; j++) {
// if bonds[j] is not an empty array
if (bonds[j].length) {
if (symbols[j] == 'H') {
return true;
}
}
}
}
}
return false;
}
/**
* Compute the molecules within the model. For internal use
* @private
*/
_computeMolecules() {
this._molecules = [];
this._molinds = [];
if (this.length < 2) {
// No molecules can be computed
this._molecules = null;
return;
}
var mol_sets = [];
var unsorted_atoms = _.range(this.length);
while (unsorted_atoms.length > 0) {
var mol_queue = [
[unsorted_atoms.shift(), [0, 0, 0]]
];
var current_mol = [];
var current_mol_cells = [];
while (mol_queue.length > 0) {
var ac1 = mol_queue.shift();
var a1 = ac1[0];
var c1 = ac1[1];
current_mol.push(a1);
current_mol_cells.push(c1);
// Find linked atoms
var link1 = this._bondmat[a1];
for (let i in link1) {
var a2 = parseInt(i);
var link12 = link1[i];
//