{
12 | if (err || !stat || stat.size < 1) {
13 | seleniumDownload.ensure(BINPATH, (error) => {
14 | if (error) throw new Error(error);
15 | console.log('✔ Selenium & Chromedriver downloaded to:', BINPATH);
16 | });
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/molecule_3d.jsx:
--------------------------------------------------------------------------------
1 | import jQuery from 'jquery';
2 | import React from 'react';
3 | import libUtils from '../utils/lib_utils';
4 | import moleculeUtils from '../utils/molecule_utils';
5 | import selectionTypesConstants from '../constants/selection_types_constants';
6 |
7 | window.$ = jQuery;
8 | const $3Dmol = require('3dmol');
9 |
10 | const DEFAULT_FONT_SIZE = 14;
11 | const ORBITAL_COLOR_POSITIVE = 0xff0000;
12 | const ORBITAL_COLOR_NEGATIVE = 0x0000ff;
13 | const ORBITAL_DEFAULT_OPACITY = 0.8;
14 |
15 | class Molecule3d extends React.Component {
16 | static defaultProps = {
17 | atomLabelsShown: false,
18 | backgroundOpacity: 1.0,
19 | backgroundColor: '#73757c',
20 | height: '500px',
21 | onRenderNewData: () => {},
22 | orbital: {},
23 | selectedAtomIds: [],
24 | selectionType: selectionTypesConstants.ATOM,
25 | shapes: [],
26 | labels: [],
27 | styles: {},
28 | width: '500px',
29 | outlineWidth: 0.0,
30 | outlineColor: '#000000',
31 | nearClip: null,
32 | farClip: null,
33 | }
34 |
35 | static propTypes = {
36 | atomLabelsShown: React.PropTypes.bool,
37 | backgroundColor: React.PropTypes.string,
38 | backgroundOpacity: React.PropTypes.number,
39 | height: React.PropTypes.string,
40 | modelData: React.PropTypes.shape({
41 | atoms: React.PropTypes.array,
42 | bonds: React.PropTypes.array,
43 | }).isRequired,
44 | onChangeSelection: React.PropTypes.func,
45 | onRenderNewData: React.PropTypes.func,
46 | orbital: React.PropTypes.shape({
47 | cube_file: React.PropTypes.string,
48 | iso_val: React.PropTypes.number,
49 | opacity: React.PropTypes.number,
50 | positiveVolumetricColor: React.PropTypes.string,
51 | negativeVolumetricColor: React.PropTypes.string,
52 | }),
53 | selectedAtomIds: React.PropTypes.arrayOf(React.PropTypes.number),
54 | selectionType: React.PropTypes.oneOf([
55 | selectionTypesConstants.ATOM,
56 | selectionTypesConstants.RESIDUE,
57 | selectionTypesConstants.CHAIN,
58 | ]),
59 | shapes: React.PropTypes.arrayOf(React.PropTypes.object),
60 | labels: React.PropTypes.arrayOf(React.PropTypes.object),
61 | styles: React.PropTypes.objectOf(React.PropTypes.object),
62 | width: React.PropTypes.string,
63 | nearClip: React.PropTypes.number,
64 | farClip: React.PropTypes.number,
65 | outlineWidth: React.PropTypes.number,
66 | outlineColor: React.PropTypes.string,
67 | }
68 |
69 | static isModelDataEmpty(modelData) {
70 | return modelData.atoms.length === 0 && modelData.bonds.length === 0;
71 | }
72 |
73 | static render3dMolModel(glviewer, modelData) {
74 | glviewer.clear();
75 |
76 | if (Molecule3d.isModelDataEmpty(modelData)) {
77 | return;
78 | }
79 |
80 | glviewer.addModel(moleculeUtils.modelDataToCDJSON(modelData), 'json', {
81 | keepH: true,
82 | });
83 |
84 | // Hack in chain and residue data, since it's not supported by chemdoodle json
85 | glviewer.getModel().selectedAtoms().forEach((atom) => {
86 | const modifiedAtom = atom;
87 | const resn = (modelData.atoms[atom.serial].residue_name || '').replace(
88 | /[0-9]+/, ''
89 | );
90 | modifiedAtom.atom = modelData.atoms[atom.serial].name;
91 | modifiedAtom.chain = modelData.atoms[atom.serial].chain;
92 | modifiedAtom.resi = modelData.atoms[atom.serial].residue_index;
93 | modifiedAtom.resn = resn;
94 | });
95 | }
96 |
97 | static render3dMolShapes(glviewer, shapes) {
98 | glviewer.removeAllShapes();
99 | shapes.forEach((shape) => {
100 | if (shape.type) {
101 | glviewer[`add${shape.type}`](libUtils.getShapeSpec(shape));
102 | }
103 | });
104 | }
105 |
106 | static render3dMolLabels(glviewer, labels) {
107 | glviewer.removeAllLabels();
108 | labels.forEach((label) => {
109 | glviewer.addLabel(label.text, label);
110 | });
111 | }
112 |
113 | static render3dMolOrbital(glviewer, orbital) {
114 | if (orbital.cube_file) {
115 | const volumeData = new $3Dmol.VolumeData(orbital.cube_file, 'cube');
116 | glviewer.addIsosurface(volumeData, {
117 | isoval: orbital.iso_val,
118 | color: orbital.positiveVolumetricColor || ORBITAL_COLOR_POSITIVE,
119 | opacity: orbital.opacity || ORBITAL_DEFAULT_OPACITY,
120 | });
121 | glviewer.addIsosurface(volumeData, {
122 | isoval: -orbital.iso_val,
123 | color: orbital.negativeVolumetricColor || ORBITAL_COLOR_NEGATIVE,
124 | opacity: orbital.opacity || ORBITAL_DEFAULT_OPACITY,
125 | });
126 | }
127 | }
128 |
129 | constructor(props) {
130 | super(props);
131 |
132 | this.state = {
133 | selectedAtomIds: props.selectedAtomIds,
134 | };
135 |
136 | this.lastOutline = { width: 0.0 };
137 | }
138 |
139 | componentDidMount() {
140 | this.render3dMol();
141 | }
142 |
143 | componentWillReceiveProps(nextProps) {
144 | this.setState({
145 | selectedAtomIds: nextProps.selectedAtomIds,
146 | });
147 | }
148 |
149 | componentDidUpdate() {
150 | this.render3dMol();
151 | }
152 |
153 | onClickAtom = (glAtom) => {
154 | const atoms = this.props.modelData.atoms;
155 | const atom = atoms[glAtom.serial];
156 | const selectionType = this.props.selectionType;
157 | const newSelectedAtomIds = moleculeUtils.addSelection(
158 | atoms,
159 | this.state.selectedAtomIds,
160 | atom,
161 | selectionType
162 | );
163 |
164 | this.setState({
165 | selectedAtomIds: newSelectedAtomIds,
166 | });
167 |
168 | if (this.props.onChangeSelection) {
169 | this.props.onChangeSelection(newSelectedAtomIds);
170 | }
171 | }
172 |
173 | render3dMol() {
174 | if (!this.glviewer && Molecule3d.isModelDataEmpty(this.props.modelData)) {
175 | return;
176 | }
177 |
178 | const glviewer = this.glviewer || $3Dmol.createViewer(jQuery(this.container), {
179 | defaultcolors: $3Dmol.elementColors.rasmol,
180 | });
181 |
182 | const renderingSameModelData = moleculeUtils.modelDataEquivalent(
183 | this.oldModelData, this.props.modelData
184 | );
185 | if (!renderingSameModelData) {
186 | this.lastStylesByAtom = null;
187 | Molecule3d.render3dMolModel(glviewer, this.props.modelData);
188 | }
189 |
190 | if (this.props.outlineWidth !== this.lastOutline.width ||
191 | this.props.outlineColor !== this.lastOutline.color) {
192 | if (this.props.outlineWidth) {
193 | this.lastOutline = {
194 | style: 'outline',
195 | width: this.props.outlineWidth,
196 | color: this.props.outlineColor,
197 | };
198 | } else {
199 | this.lastOutline = {};
200 | }
201 | glviewer.setViewStyle(this.lastOutline);
202 | }
203 |
204 | const styleUpdates = Object.create(null); // style update strings to atom ids needed
205 | const stylesByAtom = Object.create(null); // all atom ids to style string
206 | this.props.modelData.atoms.forEach((atom, i) => {
207 | const selected = this.state.selectedAtomIds.indexOf(atom.serial) !== -1;
208 | const libStyle = libUtils.getLibStyle(
209 | atom, selected, this.props.atomLabelsShown, this.props.styles[i]
210 | );
211 |
212 | if (this.props.atomLabelsShown) {
213 | glviewer.addLabel(atom.name, {
214 | fontSize: DEFAULT_FONT_SIZE,
215 | position: {
216 | x: atom.positions[0],
217 | y: atom.positions[1],
218 | z: atom.positions[2],
219 | },
220 | });
221 | }
222 |
223 | const libStyleString = JSON.stringify(libStyle);
224 | stylesByAtom[atom.serial] = libStyleString;
225 |
226 | // If the style string for this atom is the same as last time, then no
227 | // need to set it again
228 | if (this.lastStylesByAtom &&
229 | this.lastStylesByAtom[atom.serial] === libStyleString) {
230 | return;
231 | }
232 |
233 | // Initialize list of atom serials for this style string, if needed
234 | if (!styleUpdates[libStyleString]) {
235 | styleUpdates[libStyleString] = [];
236 | }
237 |
238 | styleUpdates[libStyleString].push(atom.serial);
239 | });
240 |
241 | this.lastStylesByAtom = stylesByAtom;
242 |
243 | // Set these style types using a minimum number of calls to 3DMol
244 | Object.entries(styleUpdates).forEach(([libStyleString, atomSerials]) => {
245 | glviewer.setStyle(
246 | { serial: atomSerials }, JSON.parse(libStyleString)
247 | );
248 | });
249 |
250 | Molecule3d.render3dMolShapes(glviewer, this.props.shapes);
251 | Molecule3d.render3dMolLabels(glviewer, this.props.labels);
252 | Molecule3d.render3dMolOrbital(glviewer, this.props.orbital);
253 |
254 | let customSlab = false;
255 |
256 | if (typeof (this.props.nearClip) === 'number' &&
257 | typeof (this.props.farClip) === 'number') {
258 | glviewer.setSlab(this.props.nearClip, this.props.farClip);
259 | customSlab = true;
260 | }
261 |
262 | glviewer.setBackgroundColor(
263 | libUtils.colorStringToNumber(this.props.backgroundColor),
264 | this.props.backgroundOpacity
265 | );
266 |
267 | glviewer.setClickable({}, true, this.onClickAtom);
268 | glviewer.render();
269 |
270 | if (!this.oldModelData) {
271 | glviewer.zoom();
272 | glviewer.zoomTo(0.8);
273 | }
274 |
275 |
276 | if (!renderingSameModelData) {
277 | if (!customSlab) glviewer.fitSlab();
278 | this.props.onRenderNewData(glviewer);
279 | }
280 |
281 | if (!this.glviewer) {
282 | // AMV: hack to correctly expand viewer when first rendered
283 | const self = this;
284 |
285 | const intId = setInterval(() => {
286 | // wait for canvas to be visible
287 | if (self.container.children.length > 0 && self.container.children[0].offsetParent) {
288 | glviewer.resize();
289 | clearInterval(intId);
290 | }
291 | }, 50); // polling time in ms
292 | }
293 |
294 | this.oldModelData = this.props.modelData;
295 | this.glviewer = glviewer;
296 | }
297 |
298 | render() {
299 | return (
300 | { this.container = c; }}
309 | />
310 | );
311 | }
312 | }
313 |
314 | export default Molecule3d;
315 |
--------------------------------------------------------------------------------
/src/constants/environment_constants.js:
--------------------------------------------------------------------------------
1 | import keyMirror from 'keymirror';
2 |
3 | const environmentConstants = keyMirror({
4 | DEVELOPMENT: null,
5 | });
6 |
7 | export default environmentConstants;
8 |
--------------------------------------------------------------------------------
/src/constants/selection_types_constants.js:
--------------------------------------------------------------------------------
1 | const selectionTypeConstants = {
2 | ATOM: 'Atom',
3 | RESIDUE: 'Residue',
4 | CHAIN: 'Chain',
5 | };
6 |
7 | export default selectionTypeConstants;
8 |
--------------------------------------------------------------------------------
/src/constants/shape_constants.js:
--------------------------------------------------------------------------------
1 | const shapeConstants = {
2 | ARROW: 'Arrow',
3 | SPHERE: 'Sphere',
4 | CYLINDER: 'Cylinder',
5 | };
6 |
7 | export default shapeConstants;
8 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016 Autodesk Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import Molecule3d from './components/molecule_3d.jsx';
17 |
18 | export default Molecule3d;
19 |
--------------------------------------------------------------------------------
/src/utils/lib_utils.js:
--------------------------------------------------------------------------------
1 | import shapeConstants from '../constants/shape_constants';
2 |
3 | const DEFAULT_VISUALIZATION_TYPE = 'stick';
4 |
5 | /**
6 | * Utils for working with the 3dmol.js library
7 | */
8 | const libUtils = {
9 | /**
10 | * Given a color string (like #abcdef), return its Number representation
11 | * If invalid input given, return the input
12 | * @param colorString {String}
13 | * @returns {Number}
14 | */
15 | colorStringToNumber(colorString) {
16 | if (colorString.length !== 4 && colorString.length !== 7) {
17 | return colorString;
18 | }
19 | if (colorString[0] !== '#') {
20 | return colorString;
21 | }
22 |
23 | const colorInt = parseInt(colorString.substr(1, colorString.length - 1), 16);
24 |
25 | if (isNaN(colorInt)) {
26 | return colorString;
27 | }
28 |
29 | return colorInt;
30 | },
31 |
32 | /**
33 | * Given a shape object from the main model, return a shape spec ready to go into 3Dmol.js
34 | * @param shape {Object}
35 | * @returns {Object}
36 | */
37 | getShapeSpec(shape, callback) {
38 | let color;
39 | if (shape.color) {
40 | color = libUtils.colorStringToNumber(shape.color);
41 | }
42 |
43 | const shapeSpec = Object.assign({}, {
44 | alpha: 0.8,
45 | callback,
46 | clickable: false,
47 | color: 0x00FE03,
48 | radius: shape.radius,
49 | }, shape, { color });
50 |
51 | if (shape.type === shapeConstants.ARROW) {
52 | shapeSpec.start = shape.start;
53 | shapeSpec.end = shape.end;
54 | } else if (shape.type === shapeConstants.SPHERE) {
55 | shapeSpec.center = shape.center;
56 | } else if (shape.type === shapeConstants.CYLINDER) {
57 | shapeSpec.fromCap = true;
58 | shapeSpec.toCap = true;
59 | shapeSpec.start = shape.start;
60 | shapeSpec.end = shape.end;
61 | } else {
62 | throw new Error('Invalid shape type.');
63 | }
64 |
65 | return shapeSpec;
66 | },
67 |
68 | /**
69 | * Get the style object needed by 3dMol for the given atom
70 | * @param atom {Object}
71 | * @returns {Object}
72 | */
73 | getLibStyle(atom, selected, atomLabelsShown, style = {}) {
74 | const libStyle = {};
75 | const visualizationType = style.visualization_type || DEFAULT_VISUALIZATION_TYPE;
76 |
77 | libStyle[visualizationType] = {};
78 | Object.entries(style).forEach(([styleKey, styleValue]) => {
79 | if (styleKey !== 'visualization_type') {
80 | libStyle[visualizationType][styleKey] = styleValue;
81 | }
82 | });
83 |
84 | if (selected) {
85 | libStyle[visualizationType].color = 0x1FF3FE;
86 | }
87 |
88 | if (typeof libStyle[visualizationType].color === 'string') {
89 | libStyle[visualizationType].color = libUtils.colorStringToNumber(
90 | libStyle[visualizationType].color
91 | );
92 | }
93 |
94 | return libStyle;
95 | },
96 | };
97 |
98 | export default libUtils;
99 |
--------------------------------------------------------------------------------
/src/utils/molecule_utils.js:
--------------------------------------------------------------------------------
1 | import { Set as ISet } from 'immutable';
2 | import selectionTypesConstants from '../constants/selection_types_constants';
3 |
4 | const moleculeUtils = {
5 | /**
6 | * Given molecule model data, return a JSON object in ChemDoodle format
7 | * @param modelData {Object}
8 | * @returns {String}
9 | */
10 | modelDataToCDJSON(modelData) {
11 | const atoms = modelData.atoms.map(atom => ({
12 | l: atom.elem,
13 | x: atom.positions[0],
14 | y: atom.positions[1],
15 | z: atom.positions[2],
16 | mass: atom.mass_magnitude,
17 | }));
18 |
19 | const bonds = modelData.bonds.map(bond => ({
20 | b: bond.atom1_index,
21 | e: bond.atom2_index,
22 | o: bond.bond_order,
23 | }));
24 |
25 | return {
26 | m: [{
27 | a: atoms,
28 | b: bonds,
29 | }],
30 | };
31 | },
32 |
33 | /**
34 | * Return a new selection of atoms considering a clicked atom, the current selection type, and
35 | * the currently selected atoms
36 | * @param atoms {Array of Atoms}
37 | * @param selectedAtoms {Array of Atoms}
38 | * @param clickedAtom {Atom}
39 | * @param selectionType {String}
40 | * @returns {Array of Atoms}
41 | */
42 | addSelection(atoms, selectedAtoms, clickedAtom, selectionType) {
43 | let selectedAtomsOut = selectedAtoms.slice();
44 | const clickedIndex = selectedAtoms.indexOf(clickedAtom.serial);
45 | const toggleOn = clickedIndex === -1;
46 |
47 | if (selectionType === selectionTypesConstants.ATOM) {
48 | if (toggleOn) {
49 | selectedAtomsOut.push(clickedAtom.serial);
50 | } else {
51 | selectedAtomsOut.splice(clickedIndex, 1);
52 | }
53 |
54 | return selectedAtomsOut;
55 | }
56 |
57 | if (toggleOn) {
58 | atoms.forEach((atom) => {
59 | if (moleculeUtils.isSameGroup(clickedAtom, atom, selectionType)) {
60 | selectedAtomsOut.push(atom.serial);
61 | }
62 | });
63 | } else {
64 | selectedAtomsOut = selectedAtomsOut.filter((atomSerial) => {
65 | const atom = atoms[atomSerial];
66 | return !moleculeUtils.isSameGroup(clickedAtom, atom, selectionType);
67 | });
68 | }
69 |
70 | return selectedAtomsOut;
71 | },
72 |
73 | /**
74 | * Returns a boolean indicating if the given atoms are of the same type (residue or chain)
75 | * @param atomA {Atom}
76 | * @param atomB {Atom}
77 | * @param selectionType {String}
78 | * @returns {Boolean}
79 | */
80 | isSameGroup(atomA, atomB, selectionType) {
81 | if (selectionType === selectionTypesConstants.RESIDUE) {
82 | return atomA.residue_index === atomB.residue_index;
83 | }
84 | if (selectionType === selectionTypesConstants.CHAIN) {
85 | return atomA.chain === atomB.chain;
86 | }
87 |
88 | throw new Error('selectionType must be either residue or chain');
89 | },
90 |
91 | /**
92 | * Checks to see if each modelData contains the same atoms and bonds.
93 | * Saves time by not checking every last piece of data right now.
94 | * Currently checks atom and bond ids, and atom positions
95 | * @param modalDataA {Object}
96 | * @param modelDataB {Object}
97 | * @returns {Boolean}
98 | */
99 | modelDataEquivalent(modelDataA, modelDataB) {
100 | if (!modelDataA || !modelDataB) {
101 | return false;
102 | }
103 |
104 | const atomIdsA = new ISet(modelDataA.atoms.map(atom => atom.serial));
105 | const atomIdsB = new ISet(modelDataB.atoms.map(atom => atom.serial));
106 | const bondRelationsA = new ISet(modelDataA.bonds.map(bond =>
107 | `${bond.atom1_index}=>${bond.atom2_index}`)
108 | );
109 | const bondRelationsB = new ISet(modelDataB.bonds.map(bond =>
110 | `${bond.atom1_index}=>${bond.atom2_index}`)
111 | );
112 |
113 | const haveSameAtoms = atomIdsA.equals(atomIdsB);
114 | const haveSameBonds = bondRelationsA.equals(bondRelationsB);
115 |
116 | if (!haveSameAtoms || !haveSameBonds) {
117 | return false;
118 | }
119 |
120 | const atomIdsToPositions = new Map();
121 | for (const atom of modelDataA.atoms) {
122 | atomIdsToPositions.set(atom.serial, atom.positions || []);
123 | }
124 | return modelDataB.atoms.every(atom =>
125 | atomIdsToPositions.get(atom.serial).every((position, index) => {
126 | const positionsAtomB = atom.positions || [];
127 | return positionsAtomB[index] === position;
128 | })
129 | );
130 | },
131 | };
132 |
133 | export default moleculeUtils;
134 |
--------------------------------------------------------------------------------
/test/components/molecule_3d_spec.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, before, after, beforeEach, afterEach */
2 |
3 | import React from 'react';
4 | import ReactTestUtils from 'react-addons-test-utils';
5 | import { expect } from 'chai';
6 | import sinon from 'sinon';
7 | import { mount } from 'enzyme';
8 | import Molecule3d from '../../src/components/molecule_3d';
9 | import bipyridineModelData from '../../example/js/bipyridine_model_data';
10 | import threeAidModelData from '../../example/js/3aid_model_data';
11 | import factories from '../fixtures/factories';
12 |
13 | const $3Dmol = require('3dmol');
14 |
15 | describe('Molecule3d', () => {
16 | let modelData;
17 | let modelData2;
18 | let emptyModelData;
19 | let renderer;
20 | let glViewer;
21 |
22 | beforeEach(() => {
23 | modelData = bipyridineModelData;
24 | modelData2 = threeAidModelData;
25 | emptyModelData = { atoms: [], bonds: [] };
26 | renderer = ReactTestUtils.createRenderer();
27 |
28 | glViewer = factories.getGlViewer();
29 | sinon.stub($3Dmol, 'createViewer').callsFake(() => glViewer);
30 | });
31 |
32 | afterEach(() => {
33 | $3Dmol.createViewer.restore();
34 | });
35 |
36 | describe('render', () => {
37 | it('renders a div', () => {
38 | renderer.render(React.createElement(Molecule3d, { modelData }));
39 | const result = renderer.getRenderOutput();
40 | expect(result.type).to.equal('div');
41 | });
42 | });
43 |
44 | describe('onRenderNewData', () => {
45 | it('calls onRenderNewData for initial modelData', () => {
46 | const callback = sinon.spy();
47 | mount()
48 | expect(callback.callCount).to.equal(1);
49 | expect(callback.calledWith(glViewer)).to.equal(true);
50 | });
51 |
52 | it('calls onRenderNewData when modelData changed', () => {
53 | const callback = sinon.spy();
54 | const wrapper = mount();
55 | wrapper.setProps({ modelData: modelData2 });
56 | expect(callback.callCount).to.equal(2);
57 | expect(callback.calledWith(glViewer)).to.equal(true);
58 | });
59 |
60 | it('doesn\'t call onRenderNewData when modelData not changed', () => {
61 | const callback = sinon.spy();
62 | const wrapper = mount();
63 | wrapper.setProps({ modelData, atomLabelsShown: true }); // Need a changed property to force re-render
64 | expect(callback.callCount).to.equal(1);
65 | expect(callback.calledWith(glViewer)).to.equal(true);
66 | });
67 |
68 | it('doesn\'t call onRenderNewData when empty modelData supplied', () => {
69 | const callback = sinon.spy();
70 | mount();
71 | expect(callback.callCount).to.equal(0);
72 | });
73 | });
74 |
75 | describe('render3dMol', () => {
76 | beforeEach(() => {
77 | sinon.spy(glViewer, 'addLabel');
78 | });
79 |
80 | describe('when atomLabelsShown is true', () => {
81 | beforeEach(() => {
82 | ReactTestUtils.renderIntoDocument(React.createElement(Molecule3d, {
83 | modelData,
84 | atomLabelsShown: true,
85 | }));
86 | });
87 |
88 | it('adds a label for each atom', () => {
89 | expect(glViewer.addLabel.callCount).to.equal(modelData.atoms.length);
90 | });
91 | });
92 |
93 | describe('when atomLabelsShown is false', () => {
94 | beforeEach(() => {
95 | sinon.spy(glViewer, 'removeAllLabels');
96 |
97 | ReactTestUtils.renderIntoDocument(React.createElement(Molecule3d, {
98 | modelData,
99 | atomLabelsShown: false,
100 | }));
101 | });
102 |
103 | it('removes all labels', () => {
104 | expect(glViewer.addLabel.called).to.equal(false);
105 | expect(glViewer.removeAllLabels.calledOnce).to.equal(true);
106 | });
107 | });
108 |
109 | describe('when initially loading empty modelData', () => {
110 | beforeEach(() => {
111 | modelData = {
112 | atoms: [],
113 | bonds: [],
114 | };
115 | });
116 |
117 | it('doesn\'t render glviewer', () => {
118 | const wrapper = mount();
119 | expect(wrapper.node.glviewer).to.equal(undefined);
120 | });
121 | });
122 |
123 | describe('when emptying modelData after set', () => {
124 | it('removes all viewer models', () => {
125 | const wrapper = mount();
126 | expect(wrapper.node.glviewer.getModel()).to.not.equal(null);
127 | wrapper.setProps({ modelData: emptyModelData });
128 | expect(wrapper.node.glviewer.getModel()).to.equal(null);
129 | });
130 | });
131 |
132 | describe('when reloading modelData after emptying', () => {
133 | it('removes all viewer models and adds new ones in', () => {
134 | const wrapper = mount();
135 | expect(wrapper.node.glviewer.getModel()).to.not.equal(null);
136 | wrapper.setProps({ modelData: { atoms: [], bonds: [] } });
137 | expect(wrapper.node.glviewer.getModel()).to.equal(null);
138 | wrapper.setProps({ modelData });
139 | expect(wrapper.node.glviewer.getModel()).to.not.equal(null);
140 | });
141 | });
142 |
143 | describe('when loading partially complete modelData', () => {
144 | beforeEach(() => {
145 | modelData = {
146 | atoms: modelData.atoms,
147 | bonds: [],
148 | };
149 | sinon.spy(glViewer, 'addModel');
150 | });
151 |
152 | it('tries to render', () => {
153 | const wrapper = mount();
154 | expect(wrapper.node.glviewer).to.equal(glViewer);
155 | });
156 | });
157 | });
158 | });
159 |
--------------------------------------------------------------------------------
/test/e2e/fixtures/setup.js:
--------------------------------------------------------------------------------
1 | module.exports = function setup(browser) {
2 | browser.windowSize('current', 1700, 1100);
3 |
4 | return browser;
5 | };
6 |
--------------------------------------------------------------------------------
/test/e2e/specs/startup_spec.js:
--------------------------------------------------------------------------------
1 | const setup = require('../fixtures/setup');
2 |
3 | module.exports = {
4 | 'Startup Test': (browser) => {
5 | setup(browser)
6 | .url(browser.launchUrl)
7 | .waitForElementVisible('.molecule-3d', 1000, 'molecule-3d canvas element appears')
8 | .end();
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/test/fixtures/factories.js:
--------------------------------------------------------------------------------
1 | const factories = {
2 | // 3dMol's glViewer class
3 | getGlViewer() {
4 | let model = null;
5 | return {
6 | addLabel: () => {},
7 | addModel: () => {
8 | model = {
9 | selectedAtoms: () => [],
10 | };
11 | },
12 | clear: () => {
13 | model = null;
14 | },
15 | fitSlab: () => {},
16 | getModel: () => model,
17 | removeAllLabels: () => {},
18 | removeAllShapes: () => {},
19 | render: () => {},
20 | setBackgroundColor: () => {},
21 | setClickable: () => {},
22 | setStyle: () => {},
23 | setViewStyle: () => {},
24 | zoom: () => {},
25 | zoomTo: () => {},
26 | };
27 | },
28 | };
29 |
30 | export default factories;
31 |
--------------------------------------------------------------------------------
/test/utils/lib_utils_spec.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, before, after, beforeEach, afterEach */
2 |
3 | import { expect } from 'chai';
4 | import libUtils from '../../src/utils/lib_utils';
5 |
6 | describe('libUtils', () => {
7 | describe('colorStringToNumber', () => {
8 | describe('when given invalid input', () => {
9 | it('returns the original input', () => {
10 | expect(libUtils.colorStringToNumber('blue')).to.equal('blue');
11 | expect(libUtils.colorStringToNumber('abcdef')).to.equal('abcdef');
12 | expect(libUtils.colorStringToNumber('#!bcdef')).to.equal('#!bcdef');
13 | });
14 | });
15 |
16 | describe('when given a valid hash color', () => {
17 | it('returns the Number representation', () => {
18 | expect(libUtils.colorStringToNumber('#000000')).to.equal(0);
19 | expect(libUtils.colorStringToNumber('#ffffff')).to.equal(16777215);
20 | expect(libUtils.colorStringToNumber('#abcdef')).to.equal(0xabcdef);
21 | expect(libUtils.colorStringToNumber('#bada55')).to.equal(0xbada55);
22 | });
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/utils/molecule_utils_spec.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, before, after, beforeEach, afterEach */
2 |
3 | import { expect } from 'chai';
4 | import moleculeUtils from '../../src/utils/molecule_utils';
5 | import bipyridineModelData from '../../example/js/bipyridine_model_data';
6 | import selectionTypesConstants from '../../src/constants/selection_types_constants';
7 |
8 | describe('moleculeUtils', () => {
9 | describe('modelDataToCDJSON', () => {
10 | let modelData = {};
11 |
12 | describe('when given valid modelData', () => {
13 | before(() => {
14 | modelData = bipyridineModelData;
15 | });
16 |
17 | it('returns ChemDoodle json', () => {
18 | const json = moleculeUtils.modelDataToCDJSON(modelData);
19 | const m = json.m[0];
20 |
21 | m.a.forEach((atom) => {
22 | expect(typeof atom.l).to.equal('string');
23 | expect(typeof atom.x).to.equal('number');
24 | expect(typeof atom.y).to.equal('number');
25 | expect(typeof atom.z).to.equal('number');
26 | expect(typeof atom.mass).to.equal('number');
27 | });
28 | });
29 | });
30 | });
31 |
32 | describe('addSelections', () => {
33 | let atoms;
34 | let selectedAtoms;
35 | let selectionType;
36 | let clickedAtom;
37 |
38 | beforeEach(() => {
39 | atoms = [
40 | { serial: 0, residue_index: 0, chain: 'A' },
41 | { serial: 1, residue_index: 0, chain: 'A' },
42 | { serial: 2, residue_index: 0, chain: 'A' },
43 | { serial: 3, residue_index: 1, chain: 'B' },
44 | { serial: 4, residue_index: 1, chain: 'B' },
45 | { serial: 5, residue_index: 1, chain: 'C' },
46 | ];
47 | selectedAtoms = [];
48 | });
49 |
50 | describe('when selectionType is atom', () => {
51 | beforeEach(() => {
52 | selectionType = selectionTypesConstants.ATOM;
53 | });
54 |
55 | describe('when the clicked atom is not already selected', () => {
56 | beforeEach(() => {
57 | clickedAtom = atoms[0];
58 | });
59 |
60 | it('adds the clicked atom to the selection', () => {
61 | const result = moleculeUtils.addSelection(
62 | atoms, selectedAtoms, clickedAtom, selectionType
63 | );
64 | expect(result).to.deep.equal([clickedAtom.serial]);
65 | });
66 | });
67 |
68 | describe('when the clicked atom is already selected', () => {
69 | beforeEach(() => {
70 | clickedAtom = atoms[0];
71 | selectedAtoms.push(clickedAtom.serial);
72 | });
73 |
74 | it('removes the clicked atom from the selection', () => {
75 | const result = moleculeUtils.addSelection(
76 | atoms, selectedAtoms, clickedAtom, selectionType
77 | );
78 | expect(result).to.deep.equal([]);
79 | });
80 | });
81 | });
82 |
83 | describe('when selectionType is residue', () => {
84 | beforeEach(() => {
85 | selectionType = selectionTypesConstants.RESIDUE;
86 | });
87 |
88 | describe('when the clicked atom is not already selected', () => {
89 | beforeEach(() => {
90 | clickedAtom = atoms[3];
91 | });
92 |
93 | it('adds atoms belonging to the clicked atom\'s residue to the selection', () => {
94 | const result = moleculeUtils.addSelection(
95 | atoms, selectedAtoms, clickedAtom, selectionType
96 | );
97 | expect(result).to.deep.equal([3, 4, 5]);
98 | });
99 | });
100 |
101 | describe('when the clicked atom is already selected, along with its whole residue', () => {
102 | beforeEach(() => {
103 | clickedAtom = atoms[3];
104 | selectedAtoms.push(clickedAtom.serial);
105 | selectedAtoms.push(4);
106 | selectedAtoms.push(5);
107 | });
108 |
109 | it('removes all atoms with the clicked atom\'s residue from the selection', () => {
110 | const result = moleculeUtils.addSelection(
111 | atoms, selectedAtoms, clickedAtom, selectionType
112 | );
113 | expect(result).to.deep.equal([]);
114 | });
115 | });
116 |
117 | describe('when the clicked atom is already selected, along with some of its residue', () => {
118 | beforeEach(() => {
119 | clickedAtom = atoms[3];
120 | selectedAtoms.push(clickedAtom.serial);
121 | selectedAtoms.push(5);
122 | });
123 |
124 | it('removes same residue atoms and leaves unselected ones unselected', () => {
125 | const result = moleculeUtils.addSelection(
126 | atoms, selectedAtoms, clickedAtom, selectionType
127 | );
128 | expect(result).to.deep.equal([]);
129 | });
130 | });
131 | });
132 |
133 | describe('when selectionType is chain', () => {
134 | beforeEach(() => {
135 | selectionType = selectionTypesConstants.CHAIN;
136 | });
137 |
138 | describe('when the clicked atom is not already selected', () => {
139 | beforeEach(() => {
140 | clickedAtom = atoms[3];
141 | });
142 |
143 | it('adds the clicked atom and its chain to the selection', () => {
144 | const result = moleculeUtils.addSelection(
145 | atoms, selectedAtoms, clickedAtom, selectionType
146 | );
147 | expect(result).to.deep.equal([3, 4]);
148 | });
149 | });
150 |
151 | describe('when the clicked atom and its chain are already selected', () => {
152 | beforeEach(() => {
153 | clickedAtom = atoms[3];
154 | selectedAtoms.push(clickedAtom.serial);
155 | selectedAtoms.push(4);
156 | });
157 |
158 | it('removes the clicked atom and other chain atoms from the selection', () => {
159 | const result = moleculeUtils.addSelection(
160 | atoms, selectedAtoms, clickedAtom, selectionType
161 | );
162 | expect(result).to.deep.equal([]);
163 | });
164 | });
165 | });
166 | });
167 |
168 | describe('isSameGroup', () => {
169 | let selectionType;
170 | let atomA;
171 | let atomB;
172 |
173 | beforeEach(() => {
174 | atomA = { serial: 0 };
175 | atomB = { serial: 1 };
176 | });
177 |
178 | describe('when selectionType is residue', () => {
179 | beforeEach(() => {
180 | selectionType = selectionTypesConstants.RESIDUE;
181 | });
182 |
183 | describe('when atoms are same residue', () => {
184 | beforeEach(() => {
185 | atomA.residue_index = 0;
186 | atomB.residue_index = 0;
187 | });
188 |
189 | it('returns true', () => {
190 | expect(moleculeUtils.isSameGroup(atomA, atomB, selectionType)).to.equal(true);
191 | });
192 | });
193 |
194 | describe('when atoms are different residue', () => {
195 | beforeEach(() => {
196 | atomA.residue_index = 0;
197 | atomB.residue_index = 1;
198 | });
199 |
200 | it('returns true', () => {
201 | expect(moleculeUtils.isSameGroup(atomA, atomB, selectionType)).to.equal(false);
202 | });
203 | });
204 | });
205 |
206 | describe('when selectionType is chain', () => {
207 | beforeEach(() => {
208 | selectionType = selectionTypesConstants.CHAIN;
209 | });
210 |
211 | describe('when atoms are same chain', () => {
212 | beforeEach(() => {
213 | atomA.chain = 'A';
214 | atomB.chain = 'A';
215 | });
216 |
217 | it('returns true', () => {
218 | expect(moleculeUtils.isSameGroup(atomA, atomB, selectionType)).to.equal(true);
219 | });
220 | });
221 |
222 | describe('when atoms are different residue', () => {
223 | beforeEach(() => {
224 | atomA.chain = 'A';
225 | atomB.chain = 'B';
226 | });
227 |
228 | it('returns true', () => {
229 | expect(moleculeUtils.isSameGroup(atomA, atomB, selectionType)).to.equal(false);
230 | });
231 | });
232 | });
233 | });
234 |
235 | describe('modelDataEquivalent', () => {
236 | let modelDataA;
237 | let modelDataB;
238 |
239 | beforeEach(() => {
240 | modelDataA = {
241 | atoms: [
242 | { serial: 0, positions: [] },
243 | { serial: 1, positions: [] },
244 | { serial: 2, positions: [] },
245 | ],
246 | bonds: [
247 | { atom1_index: 0, atom2_index: 1 },
248 | { atom1_index: 2, atom2_index: 1 },
249 | ],
250 | };
251 | modelDataB = {
252 | atoms: [
253 | { serial: 0 },
254 | { serial: 1 },
255 | { serial: 2 },
256 | ],
257 | bonds: [
258 | { atom1_index: 0, atom2_index: 1 },
259 | { atom1_index: 2, atom2_index: 1 },
260 | ],
261 | };
262 | });
263 |
264 | describe('when atoms and bonds are empty', () => {
265 | beforeEach(() => {
266 | modelDataA.atoms = [];
267 | modelDataB.atoms = [];
268 | modelDataA.bonds = [];
269 | modelDataB.bonds = [];
270 | });
271 |
272 | it('returns true', () => {
273 | expect(moleculeUtils.modelDataEquivalent(modelDataA, modelDataB)).to.equal(true);
274 | });
275 | });
276 |
277 | describe('when contain same atom and bonds', () => {
278 | describe('when positions are the same', () => {
279 | it('returns true', () => {
280 | expect(moleculeUtils.modelDataEquivalent(modelDataA, modelDataB)).to.equal(true);
281 | });
282 | });
283 |
284 | describe('when positions are different', () => {
285 | beforeEach(() => {
286 | modelDataA.atoms[0].positions = [3.14159265358979];
287 | });
288 |
289 | it('returns false', () => {
290 | expect(moleculeUtils.modelDataEquivalent(modelDataA, modelDataB)).to.equal(false);
291 | });
292 | });
293 | });
294 |
295 | describe('when atoms and bonds are different', () => {
296 | beforeEach(() => {
297 | modelDataA.bonds.push({ atom1_index: 0, atom2_index: 2 });
298 | });
299 |
300 | it('returns false', () => {
301 | expect(moleculeUtils.modelDataEquivalent(modelDataA, modelDataB)).to.equal(false);
302 | });
303 | });
304 | });
305 | });
306 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const InlineEnvironmentVariablesPlugin = require('inline-environment-variables-webpack-plugin');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | entry: {
6 | app: ['./src/main.js'],
7 | },
8 | output: {
9 | path: path.resolve(__dirname, 'dist'),
10 | publicPath: '/js/',
11 | filename: 'bundle.js',
12 | library: true,
13 | libraryTarget: 'commonjs2',
14 | },
15 | externals: [
16 | /^[a-z\-0-9]+$/,
17 | ],
18 | module: {
19 | loaders: [
20 | {
21 | test: /\.jsx?$/,
22 | exclude: /(node_modules|bower_components)/,
23 | loader: 'babel',
24 | }, {
25 | test: /\.jsx?$/,
26 | exclude: /(node_modules|bower_components)/,
27 | loader: 'eslint-loader',
28 | }, {
29 | test: /\.scss$/,
30 | include: /example\/css/,
31 | loaders: ['style', 'css', 'sass'],
32 | },
33 | {
34 | test: /\.json$/,
35 | loader: 'json-loader',
36 | },
37 | ],
38 | },
39 | plugins: [
40 | new InlineEnvironmentVariablesPlugin(),
41 | ],
42 | devtool: 'source-map',
43 | };
44 |
--------------------------------------------------------------------------------