molstar
Version:
A comprehensive macromolecular library.
304 lines (303 loc) • 17.1 kB
JavaScript
"use strict";
/**
* Copyright (c) 2018-2025 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>
* @author Jason Pattle <jpattle.exscientia.co.uk>
* @author Adam Midlik <midlik@gmail.com>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FocusLoci = exports.DefaultFocusLociBindings = exports.DefaultLociLabelProvider = exports.SelectLoci = exports.DefaultSelectLociBindings = exports.HighlightLoci = void 0;
const marker_action_1 = require("../../../mol-util/marker-action");
const objects_1 = require("../../../mol-plugin-state/objects");
const label_1 = require("../../../mol-theme/label");
const behavior_1 = require("../behavior");
const spine_1 = require("../../../mol-state/tree/spine");
const mol_state_1 = require("../../../mol-state");
const input_observer_1 = require("../../../mol-util/input/input-observer");
const binding_1 = require("../../../mol-util/binding");
const param_definition_1 = require("../../../mol-util/param-definition");
const loci_1 = require("../../../mol-model/loci");
const structure_1 = require("../../../mol-model/structure");
const array_1 = require("../../../mol-util/array");
const B = input_observer_1.ButtonsType;
const M = input_observer_1.ModifiersKeys;
const Trigger = binding_1.Binding.Trigger;
//
const DefaultHighlightLociBindings = {
hoverHighlightOnly: (0, binding_1.Binding)([Trigger(B.Flag.None)], 'Highlight', 'Hover element using ${triggers}'),
hoverHighlightOnlyExtend: (0, binding_1.Binding)([Trigger(B.Flag.None, M.create({ shift: true }))], 'Extend highlight', 'From selected to hovered element along polymer using ${triggers}'),
};
const HighlightLociParams = {
bindings: param_definition_1.ParamDefinition.Value(DefaultHighlightLociBindings, { isHidden: true }),
ignore: param_definition_1.ParamDefinition.Value([], { isHidden: true }),
preferAtoms: param_definition_1.ParamDefinition.Boolean(false, { description: 'Always prefer atoms over bonds' }),
mark: param_definition_1.ParamDefinition.Boolean(true)
};
exports.HighlightLoci = behavior_1.PluginBehavior.create({
name: 'representation-highlight-loci',
category: 'interaction',
ctor: class extends behavior_1.PluginBehavior.Handler {
constructor() {
super(...arguments);
this.lociMarkProvider = (interactionLoci, action) => {
if (!this.ctx.canvas3d || !this.params.mark)
return;
this.ctx.canvas3d.mark(interactionLoci, action);
};
}
getLoci(loci) {
return this.params.preferAtoms && structure_1.Bond.isLoci(loci) && loci.bonds.length === 2
? structure_1.Bond.toFirstStructureElementLoci(loci)
: loci;
}
register() {
this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy)
return;
const loci = this.getLoci(current.loci);
if (this.params.ignore.includes(loci.kind)) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: loci_1.EmptyLoci });
return;
}
let matched = false;
if (binding_1.Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
// remove repr to highlight loci everywhere on hover
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ loci });
matched = true;
}
if (binding_1.Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
// remove repr to highlight loci everywhere on hover
this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend({ loci });
matched = true;
}
if (!matched) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: loci_1.EmptyLoci });
}
});
this.ctx.managers.interactivity.lociHighlights.addProvider(this.lociMarkProvider);
}
unregister() {
this.ctx.managers.interactivity.lociHighlights.removeProvider(this.lociMarkProvider);
}
},
params: () => HighlightLociParams,
display: { name: 'Highlight Loci on Canvas' }
});
//
exports.DefaultSelectLociBindings = {
clickSelect: binding_1.Binding.Empty,
clickSelectOnly: binding_1.Binding.Empty,
clickToggle: (0, binding_1.Binding)([Trigger(B.Flag.Primary, M.create())], 'Toggle selection', 'Click on element using ${triggers}'),
clickToggleExtend: (0, binding_1.Binding)([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Toggle extended selection', 'Click on element using ${triggers} to extend selection along polymer'),
clickDeselect: binding_1.Binding.Empty,
clickDeselectAllOnEmpty: (0, binding_1.Binding)([Trigger(B.Flag.Primary, M.create())], 'Deselect all', 'Click on nothing using ${triggers}'),
};
const SelectLociParams = {
bindings: param_definition_1.ParamDefinition.Value(exports.DefaultSelectLociBindings, { isHidden: true }),
ignore: param_definition_1.ParamDefinition.Value([], { isHidden: true }),
preferAtoms: param_definition_1.ParamDefinition.Boolean(false, { description: 'Always prefer atoms over bonds' }),
mark: param_definition_1.ParamDefinition.Boolean(true)
};
exports.SelectLoci = behavior_1.PluginBehavior.create({
name: 'representation-select-loci',
category: 'interaction',
ctor: class extends behavior_1.PluginBehavior.Handler {
getLoci(loci) {
return this.params.preferAtoms && structure_1.Bond.isLoci(loci) && loci.bonds.length === 2
? structure_1.Bond.toFirstStructureElementLoci(loci)
: loci;
}
applySelectMark(ref, clear) {
const cell = this.ctx.state.data.cells.get(ref);
if (cell && objects_1.PluginStateObject.isRepresentation3D(cell.obj)) {
this.spine.current = cell;
const so = this.spine.getRootOfType(objects_1.PluginStateObject.Molecule.Structure);
if (so) {
if (clear) {
this.lociMarkProvider({ loci: structure_1.Structure.Loci(so.data) }, marker_action_1.MarkerAction.Deselect);
}
const loci = this.ctx.managers.structure.selection.getLoci(so.data);
this.lociMarkProvider({ loci }, marker_action_1.MarkerAction.Select);
}
}
}
register() {
const lociIsEmpty = (loci) => loci_1.Loci.isEmpty(loci);
const lociIsNotEmpty = (loci) => !loci_1.Loci.isEmpty(loci);
const actions = [
['clickSelect', current => this.ctx.managers.interactivity.lociSelects.select(current), lociIsNotEmpty],
['clickToggle', current => this.ctx.managers.interactivity.lociSelects.toggle(current), lociIsNotEmpty],
['clickToggleExtend', current => this.ctx.managers.interactivity.lociSelects.toggleExtend(current), lociIsNotEmpty],
['clickSelectOnly', current => this.ctx.managers.interactivity.lociSelects.selectOnly(current), lociIsNotEmpty],
['clickDeselect', current => this.ctx.managers.interactivity.lociSelects.deselect(current), lociIsNotEmpty],
['clickDeselectAllOnEmpty', () => this.ctx.managers.interactivity.lociSelects.deselectAll(), lociIsEmpty],
];
// sort the action so that the ones with more modifiers trigger sooner.
actions.sort((a, b) => {
const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
const k = x.triggers.length === 0 ? 0 : (0, array_1.arrayMax)(x.triggers.map(t => M.size(t.modifiers)));
const l = y.triggers.length === 0 ? 0 : (0, array_1.arrayMax)(y.triggers.map(t => M.size(t.modifiers)));
return l - k;
});
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy || !this.ctx.selectionMode)
return;
const loci = this.getLoci(current.loci);
if (this.params.ignore.includes(loci.kind))
return;
// only trigger the 1st action that matches
for (const [binding, action, condition] of actions) {
if (binding_1.Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(loci))) {
action({ repr: current.repr, loci });
break;
}
}
});
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
// re-apply select-mark to all representation of an updated structure
this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, oldData, action }) => {
const cell = this.ctx.state.data.cells.get(ref);
if (cell && objects_1.PluginStateObject.Molecule.Structure.is(cell.obj)) {
const structure = obj.data;
const oldStructure = action === 'recreate' ? oldObj === null || oldObj === void 0 ? void 0 : oldObj.data :
action === 'in-place' ? oldData : undefined;
if (oldStructure &&
structure_1.Structure.areEquivalent(structure, oldStructure) &&
structure_1.Structure.areHierarchiesEqual(structure, oldStructure))
return;
const reprs = this.ctx.state.data.select(mol_state_1.StateSelection.children(ref).ofType(objects_1.PluginStateObject.Molecule.Structure.Representation3D));
for (const repr of reprs)
this.applySelectMark(repr.transform.ref, true);
}
});
}
unregister() {
this.ctx.managers.interactivity.lociSelects.removeProvider(this.lociMarkProvider);
}
constructor(ctx, params) {
super(ctx, params);
this.lociMarkProvider = (reprLoci, action) => {
if (!this.ctx.canvas3d || !this.params.mark)
return;
this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
};
this.spine = new spine_1.StateTreeSpine.Impl(ctx.state.data.cells);
}
},
params: () => SelectLociParams,
display: { name: 'Select Loci on Canvas' }
});
//
exports.DefaultLociLabelProvider = behavior_1.PluginBehavior.create({
name: 'default-loci-label-provider',
category: 'interaction',
ctor: class {
register() { this.ctx.managers.lociLabels.addProvider(this.f); }
unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
constructor(ctx) {
this.ctx = ctx;
this.f = {
label: (loci) => {
const label = [];
if (structure_1.StructureElement.Loci.is(loci)) {
const entityNames = new Set();
for (const { unit: u } of loci.elements) {
const l = structure_1.StructureElement.Location.create(loci.structure, u, u.elements[0]);
const name = structure_1.StructureProperties.entity.pdbx_description(l).join(', ');
entityNames.add(name);
}
if (entityNames.size === 1)
entityNames.forEach(name => label.push(name));
}
label.push((0, label_1.lociLabel)(loci));
return label.filter(l => !!l).join('</br>');
},
group: (label) => label.toString().replace(/Model [0-9]+/g, 'Models'),
priority: 100
};
}
},
display: { name: 'Provide Default Loci Label' }
});
//
exports.DefaultFocusLociBindings = {
clickFocus: (0, binding_1.Binding)([
Trigger(B.Flag.Primary, M.create()),
], 'Representation Focus', 'Click element using ${triggers}'),
clickFocusAdd: (0, binding_1.Binding)([
Trigger(B.Flag.Primary, M.create({ control: true })),
Trigger(B.Flag.Primary, M.create({ meta: true })),
], 'Representation Focus Add', 'Click element using ${triggers}'),
clickFocusExtend: (0, binding_1.Binding)([
Trigger(B.Flag.Primary, M.create({ shift: true })),
], 'Representation Focus Extend', 'Click on element using ${triggers}'),
clickFocusSelectMode: (0, binding_1.Binding)([
// default is empty
], 'Representation Focus (Selection Mode)', 'Click element using ${triggers}'),
clickFocusAddSelectMode: (0, binding_1.Binding)([
// default is empty
], 'Representation Focus Add (Selection Mode)', 'Click element using ${triggers}'),
clickFocusExtendSelectMode: (0, binding_1.Binding)([
// default is empty
], 'Representation Focus Extend (Selection Mode)', 'Click on element using ${triggers}'),
};
const FocusLociParams = {
bindings: param_definition_1.ParamDefinition.Value(exports.DefaultFocusLociBindings, { isHidden: true }),
};
exports.FocusLoci = behavior_1.PluginBehavior.create({
name: 'representation-focus-loci',
category: 'interaction',
ctor: class extends behavior_1.PluginBehavior.Handler {
register() {
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
var _a, _b, _c, _d, _e;
const { clickFocus, clickFocusAdd, clickFocusExtend, clickFocusSelectMode, clickFocusAddSelectMode, clickFocusExtendSelectMode } = this.params.bindings;
const binding = this.ctx.selectionMode ? clickFocusSelectMode : clickFocus;
const matched = binding_1.Binding.match(binding, button, modifiers);
const bindingAdd = this.ctx.selectionMode ? clickFocusAddSelectMode : clickFocusAdd;
const matchedAdd = binding_1.Binding.match(bindingAdd, button, modifiers);
const bindingExtend = this.ctx.selectionMode ? clickFocusExtendSelectMode : clickFocusExtend;
const matchedExtend = binding_1.Binding.match(bindingExtend, button, modifiers);
if (!matched && !matchedAdd && !matchedExtend)
return;
// Support snapshot key property, in which case ignore the focus functionality
const snapshotKey = (_d = (_c = (_b = (_a = current.repr) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.snapshotKey) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '';
if (!this.ctx.selectionMode && matched && snapshotKey) {
this.ctx.managers.snapshot.applyKey(snapshotKey);
return;
}
// only apply structure focus for appropriate granularity
const { granularity } = this.ctx.managers.interactivity.props;
if (granularity !== 'residue' && granularity !== 'element')
return;
const loci = loci_1.Loci.normalize(current.loci, 'residue');
const entry = this.ctx.managers.structure.focus.current;
if (entry && loci_1.Loci.areEqual(entry.loci, loci)) {
this.ctx.managers.structure.focus.clear();
}
else {
if (matched) {
this.ctx.managers.structure.focus.setFromLoci(loci);
}
else {
if (matchedExtend) {
this.ctx.managers.structure.focus.extendFromLoci(loci);
}
else { // matchedAdd
this.ctx.managers.structure.focus.toggleFromLoci(loci);
}
// focus-add and focus-extend is not handled in camera behavior, doing it here
const current = (_e = this.ctx.managers.structure.focus.current) === null || _e === void 0 ? void 0 : _e.loci;
if (current)
this.ctx.managers.camera.focusLoci(current);
}
}
});
}
},
params: () => FocusLociParams,
display: { name: 'Representation Focus Loci on Canvas' }
});