forked from enviPath/enviPy
Current Dev State
This commit is contained in:
364
static/js/ketcher2/script/editor/index.js
Normal file
364
static/js/ketcher2/script/editor/index.js
Normal file
@ -0,0 +1,364 @@
|
||||
/****************************************************************************
|
||||
* 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;
|
||||
Reference in New Issue
Block a user