Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,642 @@
/****************************************************************************
* 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.
***************************************************************************/
// ReStruct is to store all the auxiliary information for
// Struct while rendering
var Box2Abs = require('../../util/box2abs');
var Map = require('../../util/map');
var Pool = require('../../util/pool');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var scale = require('../../util/scale');
var util = require('../util');
var Struct = require('../../chem/struct');
var ReAtom = require('./reatom');
var ReBond = require('./rebond');
var ReRxnPlus = require('./rerxnplus');
var ReRxnArrow = require('./rerxnarrow');
var ReFrag = require('./refrag');
var ReRGroup = require('./rergroup');
var ReDataSGroupData = require('./redatasgroupdata');
var ReChiralFlag = require('./rechiralflag');
var ReSGroup = require('./resgroup');
var ReLoop = require('./reloop');
var LAYER_MAP = {
background: 0,
selectionPlate: 1,
highlighting: 2,
warnings: 3,
data: 4,
indices: 5
};
function ReStruct(molecule, render) { // eslint-disable-line max-statements
this.render = render;
this.atoms = new Map();
this.bonds = new Map();
this.reloops = new Map();
this.rxnPluses = new Map();
this.rxnArrows = new Map();
this.frags = new Map();
this.rgroups = new Map();
this.sgroups = new Map();
this.sgroupData = new Map();
this.chiralFlags = new Map();
this.molecule = molecule || new Struct();
this.initialized = false;
this.layers = [];
this.initLayers();
this.connectedComponents = new Pool();
this.ccFragmentType = new Map();
for (var map in ReStruct.maps)
this[map + 'Changed'] = {};
this.structChanged = false;
// TODO: eachItem ?
molecule.atoms.each(function (aid, atom) {
this.atoms.set(aid, new ReAtom(atom));
}, this);
molecule.bonds.each(function (bid, bond) {
this.bonds.set(bid, new ReBond(bond));
}, this);
molecule.loops.each(function (lid, loop) {
this.reloops.set(lid, new ReLoop(loop));
}, this);
molecule.rxnPluses.each(function (id, item) {
this.rxnPluses.set(id, new ReRxnPlus(item));
}, this);
molecule.rxnArrows.each(function (id, item) {
this.rxnArrows.set(id, new ReRxnArrow(item));
}, this);
molecule.frags.each(function (id, item) {
this.frags.set(id, new ReFrag(item));
}, this);
molecule.rgroups.each(function (id, item) {
this.rgroups.set(id, new ReRGroup(item));
}, this);
molecule.sgroups.each(function (id, item) {
this.sgroups.set(id, new ReSGroup(item));
if (item.type === 'DAT' && !item.data.attached)
this.sgroupData.set(id, new ReDataSGroupData(item)); // [MK] sort of a hack, we use the SGroup id for the data field id
}, this);
if (molecule.isChiral) {
var bb = molecule.getCoordBoundingBox();
this.chiralFlags.set(0, new ReChiralFlag(new Vec2(bb.max.x, bb.min.y - 1)));
}
}
ReStruct.prototype.connectedComponentRemoveAtom = function (aid, atom) {
atom = atom || this.atoms.get(aid);
if (atom.component < 0)
return;
var cc = this.connectedComponents.get(atom.component);
Set.remove(cc, aid);
if (Set.size(cc) < 1)
this.connectedComponents.remove(atom.component);
atom.component = -1;
};
ReStruct.prototype.clearConnectedComponents = function () {
this.connectedComponents.clear();
this.atoms.each(function (aid, atom) {
atom.component = -1;
});
};
ReStruct.prototype.getConnectedComponent = function (aid, adjacentComponents) {
var list = (typeof (aid['length']) === 'number') ? [].slice.call(aid) : [aid];
var ids = Set.empty();
while (list.length > 0) {
(function () {
var aid = list.pop();
Set.add(ids, aid);
var atom = this.atoms.get(aid);
if (atom.component >= 0)
Set.add(adjacentComponents, atom.component);
for (var i = 0; i < atom.a.neighbors.length; ++i) {
var neiId = this.molecule.halfBonds.get(atom.a.neighbors[i]).end;
if (!Set.contains(ids, neiId))
list.push(neiId);
}
}).apply(this);
}
return ids;
};
ReStruct.prototype.addConnectedComponent = function (ids) {
var compId = this.connectedComponents.add(ids);
var adjacentComponents = Set.empty();
var atomIds = this.getConnectedComponent(Set.list(ids), adjacentComponents);
Set.remove(adjacentComponents, compId);
var type = -1;
Set.each(atomIds, function (aid) {
var atom = this.atoms.get(aid);
atom.component = compId;
if (atom.a.rxnFragmentType != -1) {
if (type != -1 && atom.a.rxnFragmentType != type)
throw new Error('reaction fragment type mismatch');
type = atom.a.rxnFragmentType;
}
}, this);
this.ccFragmentType.set(compId, type);
return compId;
};
ReStruct.prototype.removeConnectedComponent = function (ccid) {
Set.each(this.connectedComponents.get(ccid), function (aid) {
this.atoms.get(aid).component = -1;
}, this);
return this.connectedComponents.remove(ccid);
};
// TODO: remove? not used
ReStruct.prototype.connectedComponentMergeIn = function (ccid, set) {
Set.each(set, function (aid) {
this.atoms.get(aid).component = ccid;
}, this);
Set.mergeIn(this.connectedComponents.get(ccid), set);
};
ReStruct.prototype.assignConnectedComponents = function () {
this.atoms.each(function (aid, atom) {
if (atom.component >= 0)
return;
var adjacentComponents = Set.empty();
var ids = this.getConnectedComponent(aid, adjacentComponents);
Set.each(adjacentComponents, function (ccid) {
this.removeConnectedComponent(ccid);
}, this);
this.addConnectedComponent(ids);
}, this);
};
// TODO: remove? not used
ReStruct.prototype.connectedComponentGetBoundingBox = function (ccid, cc, bb) {
cc = cc || this.connectedComponents.get(ccid);
bb = bb || { min: null, max: null };
Set.each(cc, function (aid) {
var ps = scale.obj2scaled(this.atoms.get(aid).a.pp, this.render.options);
if (bb.min == null) {
bb.min = bb.max = ps;
} else {
bb.min = bb.min.min(ps);
bb.max = bb.max.max(ps);
}
}, this);
return bb;
};
ReStruct.prototype.initLayers = function () {
for (var group in LAYER_MAP) {
this.layers[LAYER_MAP[group]] =
this.render.paper.rect(0, 0, 10, 10)
.attr({
class: group + 'Layer',
fill: '#000',
opacity: '0.0'
}).toFront();
}
};
ReStruct.prototype.addReObjectPath = function (group, visel, path, pos, visible) { // eslint-disable-line max-params
if (!path || !this.layers[LAYER_MAP[group]].node.parentNode)
return;
var offset = this.render.options.offset;
var bb = visible ? Box2Abs.fromRelBox(util.relBox(path.getBBox())) : null;
var ext = pos && bb ? bb.translate(pos.negated()) : null;
if (offset !== null) {
path.translateAbs(offset.x, offset.y);
bb = bb ? bb.translate(offset) : null;
}
visel.add(path, bb, ext);
path.insertBefore(this.layers[LAYER_MAP[group]]);
};
ReStruct.prototype.clearMarks = function () {
for (var map in ReStruct.maps)
this[map + 'Changed'] = {};
this.structChanged = false;
};
ReStruct.prototype.markItemRemoved = function () {
this.structChanged = true;
};
ReStruct.prototype.markBond = function (bid, mark) {
this.markItem('bonds', bid, mark);
};
ReStruct.prototype.markAtom = function (aid, mark) {
this.markItem('atoms', aid, mark);
};
ReStruct.prototype.markItem = function (map, id, mark) {
var mapChanged = this[map + 'Changed'];
mapChanged[id] = (typeof (mapChanged[id]) !== 'undefined') ?
Math.max(mark, mapChanged[id]) : mark;
if (this[map].has(id))
this.clearVisel(this[map].get(id).visel);
};
ReStruct.prototype.clearVisel = function (visel) {
for (var i = 0; i < visel.paths.length; ++i)
visel.paths[i].remove();
visel.clear();
};
ReStruct.prototype.eachItem = function (func, context) {
for (var map in ReStruct.maps) {
this[map].each(function (id, item) {
func.call(context, item);
});
}
};
ReStruct.prototype.getVBoxObj = function (selection) {
selection = selection || {};
if (isSelectionEmpty(selection)) {
for (var map in ReStruct.maps)
if (ReStruct.maps.hasOwnProperty(map)) selection[map] = this[map].keys();
}
var vbox = null;
for (map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map) && selection[map]) {
selection[map].forEach(function (id) {
var box = this[map].get(id).getVBoxObj(this.render);
if (box)
vbox = vbox ? Box2Abs.union(vbox, box) : box.clone();
}, this);
}
}
vbox = vbox || new Box2Abs(0, 0, 0, 0);
return vbox;
};
function isSelectionEmpty(selection) {
if (selection) {
for (var map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map) && selection[map] && selection[map].length > 0)
return false;
}
}
return true;
}
ReStruct.prototype.translate = function (d) {
this.eachItem(function (item) {
item.visel.translate(d);
});
};
ReStruct.prototype.scale = function (s) {
// NOTE: bounding boxes are not valid after scaling
this.eachItem(function (item) {
scaleVisel(item.visel, s);
});
};
function scaleRPath(path, s) {
if (path.type == 'set') { // TODO: rework scaling
for (var i = 0; i < path.length; ++i)
scaleRPath(path[i], s);
} else {
if (!(typeof path.attrs === 'undefined')) {
if ('font-size' in path.attrs)
path.attr('font-size', path.attrs['font-size'] * s);
else if ('stroke-width' in path.attrs)
path.attr('stroke-width', path.attrs['stroke-width'] * s);
}
path.scale(s, s, 0, 0);
}
}
function scaleVisel(visel, s) {
for (var i = 0; i < visel.paths.length; ++i)
scaleRPath(visel.paths[i], s);
}
ReStruct.prototype.clearVisels = function () {
this.eachItem(function (item) {
this.clearVisel(item.visel);
}, this);
};
ReStruct.prototype.update = function (force) { // eslint-disable-line max-statements
force = force || !this.initialized;
// check items to update
var id, map, mapChanged;
if (force) {
for (map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map)) {
mapChanged = this[map + 'Changed'];
this[map].each(function (id) {
mapChanged[id] = 1;
}, this);
}
}
} else {
// check if some of the items marked are already gone
for (map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map)) {
mapChanged = this[map + 'Changed'];
for (id in mapChanged) {
if (!this[map].has(id)) // eslint-disable-line max-depth
delete mapChanged[id];
}
}
}
}
for (id in this.atomsChanged)
this.connectedComponentRemoveAtom(id);
// clean up empty fragments
// TODO: fragment removal should be triggered by the action responsible for the fragment contents removal and form an operation of its own
var emptyFrags = this.frags.findAll(function (fid, frag) {
return !frag.calcBBox(this.render.ctab, fid, this.render);
}, this);
for (var j = 0; j < emptyFrags.length; ++j) {
var fid = emptyFrags[j];
this.clearVisel(this.frags.get(fid).visel);
this.frags.unset(fid);
this.molecule.frags.remove(fid);
}
for (map in ReStruct.maps) {
mapChanged = this[map + 'Changed'];
for (id in mapChanged) {
this.clearVisel(this[map].get(id).visel);
this.structChanged |= mapChanged[id] > 0;
}
}
// TODO: when to update sgroup?
this.sgroups.each(function (sid, sgroup) {
this.clearVisel(sgroup.visel);
sgroup.highlighting = null;
sgroup.selectionPlate = null;
}, this);
// TODO [RB] need to implement update-on-demand for fragments and r-groups
this.frags.each(function (frid, frag) {
this.clearVisel(frag.visel);
}, this);
this.rgroups.each(function (rgid, rgroup) {
this.clearVisel(rgroup.visel);
}, this);
if (force) { // clear and recreate all half-bonds
this.clearConnectedComponents();
this.molecule.initHalfBonds();
this.molecule.initNeighbors();
}
// only update half-bonds adjacent to atoms that have moved
this.molecule.updateHalfBonds(new Map(this.atomsChanged).findAll(function (aid, status) {
return status >= 0;
}, this));
this.molecule.sortNeighbors(new Map(this.atomsChanged).findAll(function (aid, status) {
return status >= 1;
}, this));
this.assignConnectedComponents();
this.setImplicitHydrogen();
this.initialized = true;
this.verifyLoops();
var updLoops = force || this.structChanged;
if (updLoops)
this.updateLoops();
this.showLabels();
this.showBonds();
if (updLoops)
this.showLoops();
this.showReactionSymbols();
this.showSGroups();
this.showFragments();
this.showRGroups();
if (this.render.options.hideChiralFlag !== true) {
this.chiralFlags.each(function (id, item) {
item.show(this, id, this.render.options);
}, this);
}
this.clearMarks();
return true;
};
ReStruct.prototype.updateLoops = function () {
this.reloops.each(function (rlid, reloop) {
this.clearVisel(reloop.visel);
}, this);
var ret = this.molecule.findLoops();
ret.bondsToMark.forEach(function (bid) {
this.markBond(bid, 1);
}, this);
ret.newLoops.forEach(function (loopId) {
this.reloops.set(loopId, new ReLoop(this.molecule.loops.get(loopId)));
}, this);
};
ReStruct.prototype.showLoops = function () {
var options = this.render.options;
this.reloops.each(function (rlid, reloop) {
reloop.show(this, rlid, options);
}, this);
};
ReStruct.prototype.showReactionSymbols = function () {
var options = this.render.options;
var item;
var id;
for (id in this.rxnArrowsChanged) {
item = this.rxnArrows.get(id);
item.show(this, id, options);
}
for (id in this.rxnPlusesChanged) {
item = this.rxnPluses.get(id);
item.show(this, id, options);
}
};
ReStruct.prototype.showSGroups = function () {
var options = this.render.options;
this.molecule.sGroupForest.getSGroupsBFS().reverse().forEach(function (id) {
var resgroup = this.sgroups.get(id);
resgroup.show(this, id, options);
}, this);
};
ReStruct.prototype.showFragments = function () {
this.frags.each(function (id, frag) {
var path = frag.draw(this.render, id);
if (path) this.addReObjectPath('data', frag.visel, path, null, true);
// TODO fragment selection & highlighting
}, this);
};
ReStruct.prototype.showRGroups = function () {
var options = this.render.options;
this.rgroups.each(function (id, rgroup) {
rgroup.show(this, id, options);
}, this);
};
ReStruct.prototype.eachCC = function (func, type, context) {
this.connectedComponents.each(function (ccid, cc) {
if (!type || this.ccFragmentType.get(ccid) == type)
func.call(context || this, ccid, cc);
}, this);
};
// TODO: remove? not used
ReStruct.prototype.getGroupBB = function (type) {
var bb = { min: null, max: null };
this.eachCC(function (ccid, cc) {
bb = this.connectedComponentGetBoundingBox(ccid, cc, bb);
}, type, this);
return bb;
};
ReStruct.prototype.setImplicitHydrogen = function () {
// calculate implicit hydrogens for atoms that have been modified
this.molecule.setImplicitHydrogen(Object.keys(this.atomsChanged));
};
ReStruct.prototype.loopRemove = function (loopId) {
if (!this.reloops.has(loopId))
return;
var reloop = this.reloops.get(loopId);
this.clearVisel(reloop.visel);
var bondlist = [];
for (var i = 0; i < reloop.loop.hbs.length; ++i) {
var hbid = reloop.loop.hbs[i];
if (this.molecule.halfBonds.has(hbid)) {
var hb = this.molecule.halfBonds.get(hbid);
hb.loop = -1;
this.markBond(hb.bid, 1);
this.markAtom(hb.begin, 1);
bondlist.push(hb.bid);
}
}
this.reloops.unset(loopId);
this.molecule.loops.remove(loopId);
};
ReStruct.prototype.verifyLoops = function () {
var toRemove = [];
this.reloops.each(function (rlid, reloop) {
if (!reloop.isValid(this.molecule, rlid))
toRemove.push(rlid);
}, this);
for (var i = 0; i < toRemove.length; ++i)
this.loopRemove(toRemove[i]);
};
ReStruct.prototype.showLabels = function () { // eslint-disable-line max-statements
var options = this.render.options;
for (var aid in this.atomsChanged) {
var atom = this.atoms.get(aid);
atom.show(this, aid, options);
}
};
ReStruct.prototype.showBonds = function () { // eslint-disable-line max-statements
var options = this.render.options;
for (var bid in this.bondsChanged) {
var bond = this.bonds.get(bid);
bond.show(this, bid, options);
}
};
ReStruct.prototype.setSelection = function (selection) {
var redraw = (arguments.length === 0); // render.update only
for (var map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map) && ReStruct.maps[map].isSelectable()) {
this[map].each(function (id, item) {
var selected = redraw ? item.selected :
selection && selection[map] && selection[map].indexOf(id) > -1;
this.showItemSelection(item, selected);
}, this);
}
}
};
ReStruct.prototype.showItemSelection = function (item, selected) {
var exists = item.selectionPlate != null && !item.selectionPlate.removed;
// TODO: simplify me, who sets `removed`?
item.selected = selected;
if (item instanceof ReDataSGroupData) item.sgroup.selected = selected;
if (selected) {
if (!exists) {
var render = this.render;
var options = render.options;
var paper = render.paper;
item.selectionPlate = item.makeSelectionPlate(this, paper, options);
this.addReObjectPath('selectionPlate', item.visel, item.selectionPlate);
}
if (item.selectionPlate)
item.selectionPlate.show(); // TODO [RB] review
} else
if (exists && item.selectionPlate) {
item.selectionPlate.hide(); // TODO [RB] review
}
};
ReStruct.maps = {
atoms: ReAtom,
bonds: ReBond,
rxnPluses: ReRxnPlus,
rxnArrows: ReRxnArrow,
frags: ReFrag,
rgroups: ReRGroup,
sgroupData: ReDataSGroupData,
chiralFlags: ReChiralFlag,
sgroups: ReSGroup,
reloops: ReLoop
};
module.exports = Object.assign(ReStruct, {
Atom: ReAtom,
Bond: ReBond,
RxnPlus: ReRxnPlus,
RxnArrow: ReRxnArrow,
Frag: ReFrag,
RGroup: ReRGroup,
ChiralFlag: ReChiralFlag,
SGroup: ReSGroup
});