molstar
Version:
A comprehensive macromolecular library.
306 lines (305 loc) • 16.7 kB
JavaScript
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { __extends } from "tslib";
import { MarkerAction } from '../../../mol-util/marker-action';
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
import { lociLabel } from '../../../mol-theme/label';
import { PluginBehavior } from '../behavior';
import { StateTreeSpine } from '../../../mol-state/tree/spine';
import { StateSelection } from '../../../mol-state';
import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
import { Binding } from '../../../mol-util/binding';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { Bond, Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
import { arrayMax } from '../../../mol-util/array';
var B = ButtonsType;
var M = ModifiersKeys;
var Trigger = Binding.Trigger;
//
var DefaultHighlightLociBindings = {
hoverHighlightOnly: Binding([Trigger(0 /* B.Flag.None */)], 'Highlight', 'Hover element using ${triggers}'),
hoverHighlightOnlyExtend: Binding([Trigger(0 /* B.Flag.None */, M.create({ shift: true }))], 'Extend highlight', 'From selected to hovered element along polymer using ${triggers}'),
};
var HighlightLociParams = {
bindings: PD.Value(DefaultHighlightLociBindings, { isHidden: true }),
ignore: PD.Value([], { isHidden: true }),
preferAtoms: PD.Boolean(false, { description: 'Always prefer atoms over bonds' }),
mark: PD.Boolean(true)
};
export var HighlightLoci = PluginBehavior.create({
name: 'representation-highlight-loci',
category: 'interaction',
ctor: /** @class */ (function (_super) {
__extends(class_1, _super);
function class_1() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.lociMarkProvider = function (interactionLoci, action) {
if (!_this.ctx.canvas3d || !_this.params.mark)
return;
_this.ctx.canvas3d.mark(interactionLoci, action);
};
return _this;
}
class_1.prototype.getLoci = function (loci) {
return this.params.preferAtoms && Bond.isLoci(loci) && loci.bonds.length === 2
? Bond.toFirstStructureElementLoci(loci)
: loci;
};
class_1.prototype.register = function () {
var _this = this;
this.subscribeObservable(this.ctx.behaviors.interaction.hover, function (_a) {
var current = _a.current, buttons = _a.buttons, modifiers = _a.modifiers;
if (!_this.ctx.canvas3d || _this.ctx.isBusy)
return;
var loci = _this.getLoci(current.loci);
if (_this.params.ignore.includes(loci.kind)) {
_this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci });
return;
}
var matched = false;
if (Binding.match(_this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
// remove repr to highlight loci everywhere on hover
_this.ctx.managers.interactivity.lociHighlights.highlightOnly({ loci: loci });
matched = true;
}
if (Binding.match(_this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
// remove repr to highlight loci everywhere on hover
_this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend({ loci: loci });
matched = true;
}
if (!matched) {
_this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci });
}
});
this.ctx.managers.interactivity.lociHighlights.addProvider(this.lociMarkProvider);
};
class_1.prototype.unregister = function () {
this.ctx.managers.interactivity.lociHighlights.removeProvider(this.lociMarkProvider);
};
return class_1;
}(PluginBehavior.Handler)),
params: function () { return HighlightLociParams; },
display: { name: 'Highlight Loci on Canvas' }
});
//
var DefaultSelectLociBindings = {
clickSelect: Binding.Empty,
clickToggleExtend: Binding([Trigger(1 /* B.Flag.Primary */, M.create({ shift: true }))], 'Toggle extended selection', '${triggers} to extend selection along polymer'),
clickSelectOnly: Binding.Empty,
clickToggle: Binding([Trigger(1 /* B.Flag.Primary */, M.create())], 'Toggle selection', '${triggers} on element'),
clickDeselect: Binding.Empty,
clickDeselectAllOnEmpty: Binding([Trigger(1 /* B.Flag.Primary */, M.create())], 'Deselect all', 'Click on nothing using ${triggers}'),
};
var SelectLociParams = {
bindings: PD.Value(DefaultSelectLociBindings, { isHidden: true }),
ignore: PD.Value([], { isHidden: true }),
preferAtoms: PD.Boolean(false, { description: 'Always prefer atoms over bonds' }),
mark: PD.Boolean(true)
};
export var SelectLoci = PluginBehavior.create({
name: 'representation-select-loci',
category: 'interaction',
ctor: /** @class */ (function (_super) {
__extends(class_2, _super);
function class_2(ctx, params) {
var _this = _super.call(this, ctx, params) || this;
_this.lociMarkProvider = function (reprLoci, action) {
if (!_this.ctx.canvas3d || !_this.params.mark)
return;
_this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
};
_this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
return _this;
}
class_2.prototype.getLoci = function (loci) {
return this.params.preferAtoms && Bond.isLoci(loci) && loci.bonds.length === 2
? Bond.toFirstStructureElementLoci(loci)
: loci;
};
class_2.prototype.applySelectMark = function (ref, clear) {
var cell = this.ctx.state.data.cells.get(ref);
if (cell && SO.isRepresentation3D(cell.obj)) {
this.spine.current = cell;
var so = this.spine.getRootOfType(SO.Molecule.Structure);
if (so) {
if (clear) {
this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
}
var loci = this.ctx.managers.structure.selection.getLoci(so.data);
this.lociMarkProvider({ loci: loci }, MarkerAction.Select);
}
}
};
class_2.prototype.register = function () {
var _this = this;
var lociIsEmpty = function (loci) { return Loci.isEmpty(loci); };
var lociIsNotEmpty = function (loci) { return !Loci.isEmpty(loci); };
var actions = [
['clickSelect', function (current) { return _this.ctx.managers.interactivity.lociSelects.select(current); }, lociIsNotEmpty],
['clickToggle', function (current) { return _this.ctx.managers.interactivity.lociSelects.toggle(current); }, lociIsNotEmpty],
['clickToggleExtend', function (current) { return _this.ctx.managers.interactivity.lociSelects.toggleExtend(current); }, lociIsNotEmpty],
['clickSelectOnly', function (current) { return _this.ctx.managers.interactivity.lociSelects.selectOnly(current); }, lociIsNotEmpty],
['clickDeselect', function (current) { return _this.ctx.managers.interactivity.lociSelects.deselect(current); }, lociIsNotEmpty],
['clickDeselectAllOnEmpty', function () { return _this.ctx.managers.interactivity.lociSelects.deselectAll(); }, lociIsEmpty],
];
// sort the action so that the ones with more modifiers trigger sooner.
actions.sort(function (a, b) {
var x = _this.params.bindings[a[0]], y = _this.params.bindings[b[0]];
var k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(function (t) { return M.size(t.modifiers); }));
var l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(function (t) { return M.size(t.modifiers); }));
return l - k;
});
this.subscribeObservable(this.ctx.behaviors.interaction.click, function (_a) {
var current = _a.current, button = _a.button, modifiers = _a.modifiers;
if (!_this.ctx.canvas3d || _this.ctx.isBusy || !_this.ctx.selectionMode)
return;
var loci = _this.getLoci(current.loci);
if (_this.params.ignore.includes(loci.kind))
return;
// only trigger the 1st action that matches
for (var _i = 0, actions_1 = actions; _i < actions_1.length; _i++) {
var _b = actions_1[_i], binding = _b[0], action = _b[1], condition = _b[2];
if (Binding.match(_this.params.bindings[binding], button, modifiers) && (!condition || condition(loci))) {
action({ repr: current.repr, loci: loci });
break;
}
}
});
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
this.subscribeObservable(this.ctx.state.events.object.created, function (_a) {
var ref = _a.ref;
return _this.applySelectMark(ref);
});
// re-apply select-mark to all representation of an updated structure
this.subscribeObservable(this.ctx.state.events.object.updated, function (_a) {
var ref = _a.ref, obj = _a.obj, oldObj = _a.oldObj, oldData = _a.oldData, action = _a.action;
var cell = _this.ctx.state.data.cells.get(ref);
if (cell && SO.Molecule.Structure.is(cell.obj)) {
var structure = obj.data;
var oldStructure = action === 'recreate' ? oldObj === null || oldObj === void 0 ? void 0 : oldObj.data :
action === 'in-place' ? oldData : undefined;
if (oldStructure &&
Structure.areEquivalent(structure, oldStructure) &&
Structure.areHierarchiesEqual(structure, oldStructure))
return;
var reprs = _this.ctx.state.data.select(StateSelection.children(ref).ofType(SO.Molecule.Structure.Representation3D));
for (var _i = 0, reprs_1 = reprs; _i < reprs_1.length; _i++) {
var repr = reprs_1[_i];
_this.applySelectMark(repr.transform.ref, true);
}
}
});
};
class_2.prototype.unregister = function () {
this.ctx.managers.interactivity.lociSelects.removeProvider(this.lociMarkProvider);
};
return class_2;
}(PluginBehavior.Handler)),
params: function () { return SelectLociParams; },
display: { name: 'Select Loci on Canvas' }
});
//
export var DefaultLociLabelProvider = PluginBehavior.create({
name: 'default-loci-label-provider',
category: 'interaction',
ctor: /** @class */ (function () {
function class_3(ctx) {
this.ctx = ctx;
this.f = {
label: function (loci) {
var label = [];
if (StructureElement.Loci.is(loci)) {
var entityNames = new Set();
for (var _i = 0, _a = loci.elements; _i < _a.length; _i++) {
var u = _a[_i].unit;
var l = StructureElement.Location.create(loci.structure, u, u.elements[0]);
var name_1 = StructureProperties.entity.pdbx_description(l).join(', ');
entityNames.add(name_1);
}
if (entityNames.size === 1)
entityNames.forEach(function (name) { return label.push(name); });
}
label.push(lociLabel(loci));
return label.filter(function (l) { return !!l; }).join('</br>');
},
group: function (label) { return label.toString().replace(/Model [0-9]+/g, 'Models'); },
priority: 100
};
}
class_3.prototype.register = function () { this.ctx.managers.lociLabels.addProvider(this.f); };
class_3.prototype.unregister = function () { this.ctx.managers.lociLabels.removeProvider(this.f); };
return class_3;
}()),
display: { name: 'Provide Default Loci Label' }
});
//
var DefaultFocusLociBindings = {
clickFocus: Binding([
Trigger(1 /* B.Flag.Primary */, M.create()),
], 'Representation Focus', 'Click element using ${triggers}'),
clickFocusAdd: Binding([
Trigger(1 /* B.Flag.Primary */, M.create({ shift: true })),
], 'Representation Focus Add', 'Click element using ${triggers}'),
clickFocusSelectMode: Binding([
// default is empty
], 'Representation Focus', 'Click element using ${triggers}'),
clickFocusAddSelectMode: Binding([
// default is empty
], 'Representation Focus Add', 'Click element using ${triggers}'),
};
var FocusLociParams = {
bindings: PD.Value(DefaultFocusLociBindings, { isHidden: true }),
};
export var FocusLoci = PluginBehavior.create({
name: 'representation-focus-loci',
category: 'interaction',
ctor: /** @class */ (function (_super) {
__extends(class_4, _super);
function class_4() {
return _super !== null && _super.apply(this, arguments) || this;
}
class_4.prototype.register = function () {
var _this = this;
this.subscribeObservable(this.ctx.behaviors.interaction.click, function (_a) {
var _b;
var current = _a.current, button = _a.button, modifiers = _a.modifiers;
var _c = _this.params.bindings, clickFocus = _c.clickFocus, clickFocusAdd = _c.clickFocusAdd, clickFocusSelectMode = _c.clickFocusSelectMode, clickFocusAddSelectMode = _c.clickFocusAddSelectMode;
// only apply structure focus for appropriate granularity
var granularity = _this.ctx.managers.interactivity.props.granularity;
if (granularity !== 'residue' && granularity !== 'element')
return;
var binding = _this.ctx.selectionMode ? clickFocusSelectMode : clickFocus;
var matched = Binding.match(binding, button, modifiers);
var bindingAdd = _this.ctx.selectionMode ? clickFocusAddSelectMode : clickFocusAdd;
var matchedAdd = Binding.match(bindingAdd, button, modifiers);
if (!matched && !matchedAdd)
return;
var loci = Loci.normalize(current.loci, 'residue');
var entry = _this.ctx.managers.structure.focus.current;
if (entry && Loci.areEqual(entry.loci, loci)) {
_this.ctx.managers.structure.focus.clear();
}
else {
if (matched) {
_this.ctx.managers.structure.focus.setFromLoci(loci);
}
else {
_this.ctx.managers.structure.focus.addFromLoci(loci);
// focus-add is not handled in camera behavior, doing it here
var current_1 = (_b = _this.ctx.managers.structure.focus.current) === null || _b === void 0 ? void 0 : _b.loci;
if (current_1)
_this.ctx.managers.camera.focusLoci(current_1);
}
}
});
};
return class_4;
}(PluginBehavior.Handler)),
params: function () { return FocusLociParams; },
display: { name: 'Representation Focus Loci on Canvas' }
});