Files
enviPy-bayer/static/js/ketcher2/script/editor/index.js
2025-06-23 20:13:54 +02:00

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;