'use strict';
/**
* @fileoverview Class constituting the main object that plots crystals in the webpage
* @module
*/
import * as _ from 'lodash';
import {
Renderer as Renderer
} from './render.js';
import {
Loader as Loader
} from './loader.js';
import {
Model as Model
} from './model.js';
import {
ModelView as ModelView
} from './modelview.js';
import {
AtomMesh
} from './primitives';
import {
addStaticVar
} from './utils.js';
const model_parameter_defaults = {
supercell: [1, 1, 1],
molecularCrystal: false
};
/** An object providing a full interface to a renderer for crystallographic models */
class CrystVis {
/**
* An object providing a full interface to a renderer for crystallographic
* models
* @class
* @param {string} element CSS-style identifier for the HTML element to
* put the renderer in
* @param {int} width Window width
* @param {int} height Window height. If both this and width are
* set to 0, the window fits its context and
* automatically resizes with it
*/
constructor(element, width = 0, height = 0) {
// Create a renderer
this._renderer = new Renderer(element, width, height);
this._loader = new Loader();
this._models = {};
this._current_model = null;
this._current_mname = null;
this._displayed = null;
this._selected = null;
// Handling events
this._atom_click_events = {};
this._atom_click_events[CrystVis.LEFT_CLICK] = this._defaultAtomLeftClick.bind(this);
this._atom_click_events[CrystVis.LEFT_CLICK + CrystVis.SHIFT_BUTTON] = this._defaultAtomShiftLeftClick.bind(this);
this._atom_click_events[CrystVis.LEFT_CLICK + CrystVis.CTRL_BUTTON] = this._defaultAtomCtrlLeftClick.bind(this);
this._atom_click_defaults = _.cloneDeep(this._atom_click_events);
this._atom_box_event = this._defaultAtomBox.bind(this);
this._renderer.addClickListener(this._handleAtomClick.bind(this),
this._renderer._groups.model, AtomMesh);
this._renderer.addSelBoxListener(this._handleAtomBox.bind(this),
this._renderer._groups.model, AtomMesh);
// Additional options
// Hidden (need dedicated setters)
this._hsel = false; // If true, highlight the selected atoms
// Vanilla (no get/set needed)
this.cifsymtol = 1e-2; // Parameter controlling the tolerance to symmetry when loading CIF files
}
/**
* List of loaded models
* @readonly
* @type {Array}
*/
get modelList() {
return Object.keys(this._models);
}
/**
* Currently loaded model
* @readonly
* @type {Model}
*/
get model() {
return this._current_model;
}
/**
* Name of the currently loaded model
* @readonly
* @type {String}
*/
get modelName() {
return this._current_mname;
}
/**
* Displayed atoms
* @type {ModelView}
*/
get displayed() {
return this._displayed;
}
set displayed(d) {
if (!(d instanceof ModelView)) {
throw new Error('.displayed must be set with a ModelView');
}
this._displayed.hide();
this._displayed = d;
this._displayed.show();
}
/**
* Selected atoms
* @type {ModelView}
*/
get selected() {
return this._selected;
}
set selected(s) {
if (!(s instanceof ModelView)) {
throw new Error('.selected must be set with a ModelView');
}
this._selected.setProperty('highlighted', false);
this._selected = s;
this._selected.setProperty('highlighted', this._hsel);
}
/** Whether the selected atoms should be highlighted with auras
* @type {bool}
*/
get highlightSelected() {
return this._hsel;
}
set highlightSelected(hs) {
this._hsel = hs;
if (this._selected) {
this._selected.setProperty('highlighted', this._hsel);
}
}
/**
* Set a callback function for an event where a user clicks on an atom. The
* function should take as arguments the atom image for the clicked atom and
* the event object:
*
* function callback(atom, event) {
* ...
* }
*
* @param {Function} callback Callback function for the event. Passing "null" restores default behaviour
* @param {int} modifiers Click event. Use the following flags to define it:
*
* * CrystVis.LEFT_CLICK
* * CrystVis.RIGHT_CLICK
* * CrystVis.MIDDLE_CLICK
* * CrystVis.CTRL_BUTTON
* * CrystVis.ALT_BUTTON
* * CrystVis.SHIFT_BUTTON
* * CrystVis.CMD_BUTTON
*
* For example, CrystVis.LEFT_CLICK + CrystVis.SHIFT_BUTTON
* defines the event for a click while the Shift key is pressed.
*
*/
onAtomClick(callback = null, modifiers = CrystVis.LEFT_CLICK) {
// Check that event makes sense
var lc = modifiers & CrystVis.LEFT_CLICK;
var mc = modifiers & CrystVis.MIDDLE_CLICK;
var rc = modifiers & CrystVis.RIGHT_CLICK;
if (lc + mc + rc == 0) {
throw 'Can not set event without any click type';
}
if ((lc && mc) || (lc && rc) || (mc && rc)) {
throw 'Can not set event with two or more click types';
}
if (callback)
this._atom_click_events[modifiers] = callback.bind(this);
else
this._atom_click_events[modifiers] = this._atom_click_defaults[modifiers];
}
/**
* Set a callback function for an event where a user drags a box around multiple atoms.
* The function should take as arguments a ModelView including the atoms in the box:
*
* function callback(view) {
* ...
* }
*
* @param {Function} callback Callback function for the event. Passing "null" restores default behaviour
*/
onAtomBox(callback = null) {
if (callback)
this._atom_box_event = callback;
else
this._atom_box_event = this._defaultAtomBox.bind(this);
}
_defaultAtomLeftClick(atom, event) {
var i = atom.imgIndex;
this.selected = new ModelView(this._current_model, [i]);
}
_defaultAtomShiftLeftClick(atom, event) {
var i = atom.imgIndex;
this.selected = this.selected.or(new ModelView(this._current_model, [i]));
}
_defaultAtomCtrlLeftClick(atom, event) {
var i = atom.imgIndex;
this.selected = this.selected.xor(new ModelView(this._current_model, [i]));
}
_defaultAtomBox(view) {
this.selected = this.selected.xor(view);
console.log(view);
}
// Callback for when atoms are clicked
_handleAtomClick(alist, event) {
if (alist.length == 0) {
return;
}
let clicked = alist[0].image;
let modifiers = [CrystVis.LEFT_CLICK, CrystVis.MIDDLE_CLICK, CrystVis.RIGHT_CLICK][event.button];
modifiers += event.shiftKey * CrystVis.SHIFT_BUTTON;
modifiers += (event.ctrlKey || event.metaKey) * CrystVis.CTRL_BUTTON;
modifiers += event.altKey * CrystVis.ALT_BUTTON;
var callback = this._atom_click_events[modifiers];
if (callback)
callback(clicked, event);
}
// Callback for a whole box dragged over atoms
_handleAtomBox(alist) {
var indices = alist.map(function(a) {
return a.image.imgIndex;
});
var callback = this._atom_box_event;
if (callback)
callback(new ModelView(this._current_model, indices));
}
/**
* Center the camera on a given point
*
* @param {float[]} center Point in model space that the orbiting camera
* should be centred on and look at
* @param {float[]} shift Shift (in units of width/height of the canvas) with
* which the center of the camera should be rendered with
* respect to the center of the canvas
*/
centerCamera(center = [0, 0, 0], shift = [0, 0]) {
const renderer = this._renderer;
renderer.resetOrbitCenter(center[0], center[1], center[2]);
renderer.resetCameraCenter(shift[0], shift[1]);
}
/**
* Load one or more atomic models from a file's contents
*
* @param {String} contents The contents of the structure file
* @param {String} format The file's format (cif, xyz, etc.). Default is cif.
* @param {String} prefix Prefix to use when naming the models. Default is empty.
* @param {Object} parameters Loading parameters:
*
* - `supercell`: supercell size (only used if the structure is periodic)
* - `molecularCrystal`: if true, try to make the model load completing molecules across periodic boundaries
* - `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
*
* @return {Object} Names of the models we tried to load, and values of true/false for successful loading or not
*/
loadModels(contents, format = 'cif', prefix = null, parameters = {}) {
parameters = _.merge(model_parameter_defaults, parameters);
// By default, it's cif
format = format.toLowerCase();
// By default, same as the format
prefix = prefix || format;
var structs = this._loader.load(contents, format, prefix);
var status = {};
if (this._loader.status == Loader.STATUS_ERROR) {
status[prefix] = this._loader.error_message;
return status;
}
// Now make unique names
for (var n in structs) {
var iter = 0;
var coll = true;
var nn = n;
while (coll) {
nn = n + (iter > 0 ? '_' + iter : '');
coll = nn in this._models;
iter++;
}
var s = structs[n];
if (!s) {
status[nn] = 'Model could not load properly';
continue;
}
this._models[nn] = new Model(s, parameters);
status[nn] = 0; // Success
}
return status;
}
/**
* Reload a model, possibly with new parameters
*
* @param {String} name Name of the model to reload.
* @param {Object} parameters Loading parameters as in .loadModels()
*/
reloadModel(name, parameters = {}) {
if (!(name in this._models)) {
throw 'The requested model does not exist';
}
var current = (this._current_mname == name);
if (current) {
// Hide the model to reload it later
this.displayModel();
}
var s = this._models[name]._atoms_base;
parameters = _.merge(model_parameter_defaults, parameters);
this._models[name] = new Model(s, parameters);
if (current) {
this.displayModel(name);
}
}
/**
* Render a model
*
* @param {String} name Name of the model to display. If empty, just
* clear the renderer window.
*/
displayModel(name = null) {
if (this._current_model) {
this.selected = this._current_model.view([]);
this._current_model.renderer = null;
this._current_model = null;
this._current_mname = null;
}
this._renderer.clear();
if (!name) {
// If called with nothing, just quit here
return;
}
if (!(name in this._models)) {
throw 'The requested model does not exist';
}
var m = this._models[name];
m.renderer = this._renderer;
this._current_model = m;
this._current_mname = name;
this._displayed = m.find({
'cell': [
[0, 0, 0]
]
});
this._selected = new ModelView(m, []); // Empty
// Set the camera in a way that will center the model
var c = m.fracToAbs([0.5, 0.5, 0.5]);
this._renderer.resetOrbitCenter(c[0], c[1], c[2]);
this._displayed.show();
}
/**
* Erase a model from the recorded ones
*
* @param {String} name Name of the model to delete
*/
deleteModel(name) {
if (!(name in this._models)) {
throw 'The requested model does not exist';
}
if (this._current_mname == name) {
this.displayModel();
}
delete this._models[name];
}
/**
* Add a primitive shape to the drawing
*
* @param {THREE.Object3D} p Primitive to add
*/
addPrimitive(p) {
this._renderer.add(p);
}
/**
* Remove a primitive shape from the drawing
*
* @param {THREE.Object3D} p Primitive to remove
*/
removePrimitive(p) {
this._renderer.remove(p);
}
/**
* Recover a data URL of a PNG screenshot of the current scene
*
* @return {String} A data URL of the PNG screenshot
*/
getScreenshotData() {
// Force a render
this._renderer._render();
// Grab the data from the canvas
return this._renderer._r.domElement.toDataURL();
}
}
addStaticVar(CrystVis, 'LEFT_CLICK', 1);
addStaticVar(CrystVis, 'MIDDLE_CLICK', 2);
addStaticVar(CrystVis, 'RIGHT_CLICK', 4);
addStaticVar(CrystVis, 'ALT_BUTTON', 8);
addStaticVar(CrystVis, 'CTRL_BUTTON', 16);
addStaticVar(CrystVis, 'CMD_BUTTON', 16); // Alias for Mac users
addStaticVar(CrystVis, 'SHIFT_BUTTON', 32);
export {
CrystVis
}