forked from enviPath/enviPy
365 lines
11 KiB
JavaScript
365 lines
11 KiB
JavaScript
/****************************************************************************
|
|
* Copyright 2017 EPAM Systems
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
***************************************************************************/
|
|
|
|
var s = require('subscription');
|
|
|
|
var Set = require('../util/set');
|
|
var Vec2 = require('../util/vec2');
|
|
|
|
var Struct = require('../chem/struct');
|
|
|
|
var Render = require('../render');
|
|
var Action = require('./action');
|
|
|
|
var closest = require('./closest');
|
|
|
|
var toolMap = {
|
|
rgroupatom: require('./tool/rgroupatom'),
|
|
select: require('./tool/select'),
|
|
sgroup: require('./tool/sgroup'),
|
|
eraser: require('./tool/eraser'),
|
|
atom: require('./tool/atom'),
|
|
bond: require('./tool/bond'),
|
|
chain: require('./tool/chain'),
|
|
chiralFlag: require('./tool/chiral-flag'),
|
|
template: require('./tool/template'),
|
|
charge: require('./tool/charge'),
|
|
rgroupfragment: require('./tool/rgroupfragment'),
|
|
apoint: require('./tool/apoint'),
|
|
attach: require('./tool/attach'),
|
|
reactionarrow: require('./tool/reactionarrow'),
|
|
reactionplus: require('./tool/reactionplus'),
|
|
reactionmap: require('./tool/reactionmap'),
|
|
reactionunmap: require('./tool/reactionunmap'),
|
|
paste: require('./tool/paste'),
|
|
rotate: require('./tool/rotate')
|
|
};
|
|
|
|
const SCALE = 40; // const
|
|
const HISTORY_SIZE = 32; // put me to options
|
|
|
|
var structObjects = ['atoms', 'bonds', 'frags', 'sgroups', 'sgroupData', 'rgroups', 'rxnArrows', 'rxnPluses', 'chiralFlags'];
|
|
|
|
function Editor(clientArea, options) {
|
|
this.render = new Render(clientArea, Object.assign({
|
|
scale: SCALE
|
|
}, options));
|
|
|
|
this._selection = null; // eslint-disable-line
|
|
this._tool = null; // eslint-disable-line
|
|
this.historyStack = [];
|
|
this.historyPtr = 0;
|
|
|
|
this.event = {
|
|
message: new s.Subscription(),
|
|
elementEdit: new s.PipelineSubscription(),
|
|
bondEdit: new s.PipelineSubscription(),
|
|
rgroupEdit: new s.PipelineSubscription(),
|
|
sgroupEdit: new s.PipelineSubscription(),
|
|
sdataEdit: new s.PipelineSubscription(),
|
|
quickEdit: new s.PipelineSubscription(),
|
|
attachEdit: new s.PipelineSubscription(),
|
|
change: new s.PipelineSubscription(),
|
|
selectionChange: new s.PipelineSubscription()
|
|
};
|
|
|
|
domEventSetup(this, clientArea);
|
|
}
|
|
|
|
Editor.prototype.tool = function (name, opts) {
|
|
/* eslint-disable no-underscore-dangle */
|
|
if (arguments.length > 0) {
|
|
if (this._tool && this._tool.cancel)
|
|
this._tool.cancel();
|
|
var tool = toolMap[name](this, opts);
|
|
if (!tool)
|
|
return null;
|
|
this._tool = tool;
|
|
}
|
|
return this._tool;
|
|
/* eslint-enable no-underscore-dangle */
|
|
};
|
|
|
|
Editor.prototype.struct = function (value) {
|
|
if (arguments.length > 0) {
|
|
this.selection(null);
|
|
this.update(Action.fromNewCanvas(this.render.ctab,
|
|
value || new Struct()));
|
|
recoordinate(this, getStructCenter(this.render.ctab));
|
|
}
|
|
return this.render.ctab.molecule;
|
|
};
|
|
|
|
Editor.prototype.options = function (value) {
|
|
if (arguments.length > 0) {
|
|
var struct = this.render.ctab.molecule;
|
|
var zoom = this.render.options.zoom;
|
|
this.render.clientArea.innerHTML = '';
|
|
this.render = new Render(this.render.clientArea, Object.assign({ scale: SCALE }, value));
|
|
this.render.setMolecule(struct); // TODO: reuse this.struct here?
|
|
this.render.setZoom(zoom);
|
|
this.render.update();
|
|
}
|
|
return this.render.options;
|
|
};
|
|
|
|
Editor.prototype.zoom = function (value) {
|
|
if (arguments.length > 0) {
|
|
this.render.setZoom(value);
|
|
recoordinate(this, getStructCenter(this.render.ctab,
|
|
this.selection()));
|
|
this.render.update();
|
|
}
|
|
return this.render.options.zoom;
|
|
};
|
|
|
|
Editor.prototype.selection = function (ci) {
|
|
var restruct = this.render.ctab;
|
|
if (arguments.length > 0) {
|
|
this._selection = null; // eslint-disable-line
|
|
if (ci === 'all') { // TODO: better way will be this.struct()
|
|
ci = structObjects.reduce(function (res, key) {
|
|
res[key] = restruct[key].ikeys();
|
|
return res;
|
|
}, {});
|
|
}
|
|
|
|
if (ci === 'descriptors') {
|
|
restruct = this.render.ctab;
|
|
ci = { sgroupData: restruct['sgroupData'].ikeys() };
|
|
}
|
|
|
|
if (ci) {
|
|
var res = {};
|
|
for (var key in ci) {
|
|
if (ci.hasOwnProperty(key) && ci[key].length > 0) // TODO: deep merge
|
|
res[key] = ci[key].slice();
|
|
}
|
|
if (Object.keys(res) !== 0)
|
|
this._selection = res; // eslint-disable-line
|
|
}
|
|
|
|
this.render.ctab.setSelection(this._selection); // eslint-disable-line
|
|
this.event.selectionChange.dispatch(this._selection); // eslint-disable-line
|
|
|
|
this.render.update();
|
|
}
|
|
return this._selection; // eslint-disable-line
|
|
};
|
|
|
|
Editor.prototype.hover = function (ci) {
|
|
var tool = this._tool; // eslint-disable-line
|
|
if ('ci' in tool && (!ci || tool.ci.map !== ci.map || tool.ci.id !== ci.id)) {
|
|
this.highlight(tool.ci, false);
|
|
delete tool.ci;
|
|
}
|
|
if (ci && this.highlight(ci, true))
|
|
tool.ci = ci;
|
|
};
|
|
|
|
Editor.prototype.highlight = function (ci, visible) {
|
|
if (['atoms', 'bonds', 'rxnArrows', 'rxnPluses', 'chiralFlags', 'frags',
|
|
'rgroups', 'sgroups', 'sgroupData'].indexOf(ci.map) === -1)
|
|
return false;
|
|
|
|
var rnd = this.render;
|
|
var item = rnd.ctab[ci.map].get(ci.id);
|
|
if (!item)
|
|
return true; // TODO: fix, attempt to highlight a deleted item
|
|
if ((ci.map === 'sgroups' && item.item.type === 'DAT') || ci.map === 'sgroupData') {
|
|
// set highlight for both the group and the data item
|
|
var item1 = rnd.ctab.sgroups.get(ci.id);
|
|
var item2 = rnd.ctab.sgroupData.get(ci.id);
|
|
if (item1)
|
|
item1.setHighlight(visible, rnd);
|
|
if (item2)
|
|
item2.setHighlight(visible, rnd);
|
|
} else {
|
|
item.setHighlight(visible, rnd);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
Editor.prototype.update = function (action, ignoreHistory) {
|
|
if (action === true) {
|
|
this.render.update(true); // force
|
|
} else {
|
|
if (!ignoreHistory && !action.isDummy()) {
|
|
this.historyStack.splice(this.historyPtr, HISTORY_SIZE + 1, action);
|
|
if (this.historyStack.length > HISTORY_SIZE)
|
|
this.historyStack.shift();
|
|
this.historyPtr = this.historyStack.length;
|
|
this.event.change.dispatch(action); // TODO: stoppable here
|
|
}
|
|
this.render.update();
|
|
}
|
|
};
|
|
|
|
Editor.prototype.historySize = function () {
|
|
return {
|
|
undo: this.historyPtr,
|
|
redo: this.historyStack.length - this.historyPtr
|
|
};
|
|
};
|
|
|
|
Editor.prototype.undo = function () {
|
|
if (this.historyPtr === 0)
|
|
throw new Error('Undo stack is empty');
|
|
|
|
if (this.tool() && this.tool().cancel)
|
|
this.tool().cancel();
|
|
this.selection(null);
|
|
this.historyPtr--;
|
|
var action = this.historyStack[this.historyPtr].perform(this.render.ctab);
|
|
this.historyStack[this.historyPtr] = action;
|
|
this.event.change.dispatch(action);
|
|
this.render.update();
|
|
};
|
|
|
|
Editor.prototype.redo = function () {
|
|
if (this.historyPtr === this.historyStack.length)
|
|
throw new Error('Redo stack is empty');
|
|
|
|
if (this.tool() && this.tool().cancel)
|
|
this.tool().cancel();
|
|
this.selection(null);
|
|
var action = this.historyStack[this.historyPtr].perform(this.render.ctab);
|
|
this.historyStack[this.historyPtr] = action;
|
|
this.historyPtr++;
|
|
this.event.change.dispatch(action);
|
|
this.render.update();
|
|
};
|
|
|
|
Editor.prototype.on = function (eventName, handler) {
|
|
this.event[eventName].add(handler);
|
|
};
|
|
|
|
function domEventSetup(editor, clientArea) {
|
|
// TODO: addEventListener('resize', ...);
|
|
['click', 'dblclick', 'mousedown', 'mousemove',
|
|
'mouseup', 'mouseleave'].forEach(eventName => {
|
|
const subs = editor.event[eventName] = new s.DOMSubscription();
|
|
clientArea.addEventListener(eventName, subs.dispatch.bind(subs));
|
|
|
|
subs.add(event => {
|
|
editor.lastEvent = event;
|
|
if (editor.tool() && eventName in editor.tool())
|
|
editor.tool()[eventName](event);
|
|
return true;
|
|
}, -1);
|
|
});
|
|
}
|
|
|
|
Editor.prototype.findItem = function (event, maps, skip) {
|
|
var pos = global._ui_editor ? new Vec2(this.render.page2obj(event)) : // eslint-disable-line
|
|
new Vec2(event.pageX, event.pageY).sub(elementOffset(this.render.clientArea));
|
|
var options = this.render.options;
|
|
|
|
return closest.item(this.render.ctab, pos, maps, skip, options.scale);
|
|
};
|
|
|
|
Editor.prototype.explicitSelected = function () {
|
|
var selection = this.selection() || {};
|
|
var res = structObjects.reduce(function (res, key) {
|
|
res[key] = selection[key] ? selection[key].slice() : [];
|
|
return res;
|
|
}, {});
|
|
|
|
var struct = this.render.ctab.molecule;
|
|
// "auto-select" the atoms for the bonds in selection
|
|
if ('bonds' in res) {
|
|
res.bonds.forEach(function (bid) {
|
|
var bond = struct.bonds.get(bid);
|
|
res.atoms = res.atoms || [];
|
|
if (res.atoms.indexOf(bond.begin) < 0) res.atoms.push(bond.begin);
|
|
if (res.atoms.indexOf(bond.end) < 0) res.atoms.push(bond.end);
|
|
});
|
|
}
|
|
// "auto-select" the bonds with both atoms selected
|
|
if ('atoms' in res && 'bonds' in res) {
|
|
struct.bonds.each(function (bid) {
|
|
if (!('bonds' in res) || res.bonds.indexOf(bid) < 0) {
|
|
var bond = struct.bonds.get(bid);
|
|
if (res.atoms.indexOf(bond.begin) >= 0 && res.atoms.indexOf(bond.end) >= 0) {
|
|
res.bonds = res.bonds || [];
|
|
res.bonds.push(bid);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return res;
|
|
};
|
|
|
|
Editor.prototype.structSelected = function () {
|
|
var struct = this.render.ctab.molecule;
|
|
var selection = this.explicitSelected();
|
|
var dst = struct.clone(Set.fromList(selection.atoms),
|
|
Set.fromList(selection.bonds), true);
|
|
|
|
// Copy by its own as Struct.clone doesn't support
|
|
// arrows/pluses id sets
|
|
struct.rxnArrows.each(function (id, item) {
|
|
if (selection.rxnArrows.indexOf(id) != -1)
|
|
dst.rxnArrows.add(item.clone());
|
|
});
|
|
struct.rxnPluses.each(function (id, item) {
|
|
if (selection.rxnPluses.indexOf(id) != -1)
|
|
dst.rxnPluses.add(item.clone());
|
|
});
|
|
dst.isChiral = struct.isChiral;
|
|
|
|
// TODO: should be reaction only if arrwos? check this logic
|
|
dst.isReaction = struct.isReaction &&
|
|
(dst.rxnArrows.count() || dst.rxnPluses.count());
|
|
|
|
return dst;
|
|
};
|
|
|
|
Editor.prototype.alignDescriptors = function () {
|
|
this.selection(null);
|
|
const action = Action.fromDescriptorsAlign(this.render.ctab);
|
|
this.update(action);
|
|
this.render.update(true);
|
|
};
|
|
|
|
function recoordinate(editor, rp/* , vp*/) {
|
|
// rp is a point in scaled coordinates, which will be positioned
|
|
// vp is the point where the reference point should now be (in view coordinates)
|
|
// or the center if not set
|
|
console.assert(rp, 'Reference point not specified');
|
|
editor.render.setScrollOffset(0, 0);
|
|
}
|
|
|
|
function getStructCenter(restruct, selection) {
|
|
var bb = restruct.getVBoxObj(selection || {});
|
|
return Vec2.lc2(bb.p0, 0.5, bb.p1, 0.5);
|
|
}
|
|
|
|
// TODO: find DOM shorthand
|
|
function elementOffset(element) {
|
|
var top = 0,
|
|
left = 0;
|
|
do {
|
|
top += element.offsetTop || 0;
|
|
left += element.offsetLeft || 0;
|
|
element = element.offsetParent;
|
|
} while (element);
|
|
return new Vec2(left, top);
|
|
}
|
|
|
|
module.exports = Editor;
|