molstar
Version:
A comprehensive macromolecular library.
522 lines (521 loc) • 20.3 kB
JavaScript
/**
* Copyright (c) 2019-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 Adam Midlik <midlik@gmail.com>
*/
import { OrderedSet } from '../../../mol-data/int';
import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { QueryContext, Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
import { StateObjectRef, StateSelection } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { structureElementStatsLabel } from '../../../mol-theme/label';
import { arrayRemoveAtInPlace } from '../../../mol-util/array';
import { StatefulPluginComponent } from '../../component';
import { PluginStateObject as PSO } from '../../objects';
import { UUID } from '../../../mol-util';
import { iterableToArray } from '../../../mol-data/util';
const boundaryHelper = new BoundaryHelper('98');
const HISTORY_CAPACITY = 24;
export class StructureSelectionManager extends StatefulPluginComponent {
get entries() { return this.state.entries; }
get additionsHistory() { return this.state.additionsHistory; }
get stats() {
if (this.state.stats)
return this.state.stats;
this.state.stats = this.calcStats();
return this.state.stats;
}
getEntry(s) {
// ignore decorators to get stable ref
const cell = this.plugin.helpers.substructureParent.get(s, true);
if (!cell)
return;
const ref = cell.transform.ref;
if (!this.entries.has(ref)) {
const entry = new SelectionEntry(StructureElement.Loci(s, []));
this.entries.set(ref, entry);
return entry;
}
return this.entries.get(ref);
}
calcStats() {
let structureCount = 0;
let elementCount = 0;
const stats = StructureElement.Stats.create();
this.entries.forEach(v => {
const { elements } = v.selection;
if (elements.length) {
structureCount += 1;
for (let i = 0, il = elements.length; i < il; ++i) {
elementCount += OrderedSet.size(elements[i].indices);
}
StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(v.selection));
}
});
const label = structureElementStatsLabel(stats, { countsOnly: true });
return { structureCount, elementCount, label };
}
add(loci) {
if (!StructureElement.Loci.is(loci))
return false;
const entry = this.getEntry(loci.structure);
if (!entry)
return false;
const sel = entry.selection;
entry.selection = StructureElement.Loci.union(entry.selection, loci);
this.tryAddHistory(loci);
this.referenceLoci = loci;
this.events.loci.add.next(loci);
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
remove(loci) {
if (!StructureElement.Loci.is(loci))
return false;
const entry = this.getEntry(loci.structure);
if (!entry)
return false;
const sel = entry.selection;
entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
// this.addHistory(loci);
this.referenceLoci = loci;
this.events.loci.remove.next(loci);
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
intersect(loci) {
if (!StructureElement.Loci.is(loci))
return false;
const entry = this.getEntry(loci.structure);
if (!entry)
return false;
const sel = entry.selection;
entry.selection = StructureElement.Loci.intersect(entry.selection, loci);
// this.addHistory(loci);
this.referenceLoci = loci;
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
set(loci) {
if (!StructureElement.Loci.is(loci))
return false;
const entry = this.getEntry(loci.structure);
if (!entry)
return false;
const sel = entry.selection;
entry.selection = loci;
this.tryAddHistory(loci);
this.referenceLoci = undefined;
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
modifyHistory(entry, action, modulus, groupByStructure = false) {
const history = this.additionsHistory;
const idx = history.indexOf(entry);
if (idx < 0)
return;
let swapWith = void 0;
switch (action) {
case 'remove':
arrayRemoveAtInPlace(history, idx);
break;
case 'up':
swapWith = idx - 1;
break;
case 'down':
swapWith = idx + 1;
break;
}
if (swapWith !== void 0) {
const mod = modulus ? Math.min(history.length, modulus) : history.length;
while (true) {
swapWith = swapWith % mod;
if (swapWith < 0)
swapWith += mod;
if (!groupByStructure || history[idx].loci.structure === history[swapWith].loci.structure) {
const t = history[idx];
history[idx] = history[swapWith];
history[swapWith] = t;
break;
}
else {
swapWith += action === 'up' ? -1 : +1;
}
}
}
this.events.additionsHistoryUpdated.next(void 0);
}
tryAddHistory(loci) {
if (Loci.isEmpty(loci))
return;
let idx = 0, entry = void 0;
for (const l of this.additionsHistory) {
if (Loci.areEqual(l.loci, loci)) {
entry = l;
break;
}
idx++;
}
if (entry) {
// move to top
arrayRemoveAtInPlace(this.additionsHistory, idx);
this.additionsHistory.unshift(entry);
this.events.additionsHistoryUpdated.next(void 0);
return;
}
const stats = StructureElement.Stats.ofLoci(loci);
const label = structureElementStatsLabel(stats, { reverse: true });
this.additionsHistory.unshift({ id: UUID.create22(), loci, label });
if (this.additionsHistory.length > HISTORY_CAPACITY)
this.additionsHistory.pop();
this.events.additionsHistoryUpdated.next(void 0);
}
clearHistory() {
if (this.state.additionsHistory.length !== 0) {
this.state.additionsHistory = [];
this.events.additionsHistoryUpdated.next(void 0);
}
}
clearHistoryForStructure(structure) {
const historyEntryToRemove = [];
for (const e of this.state.additionsHistory) {
if (e.loci.structure.root === structure.root) {
historyEntryToRemove.push(e);
}
}
for (const e of historyEntryToRemove) {
this.modifyHistory(e, 'remove');
}
if (historyEntryToRemove.length !== 0) {
this.events.additionsHistoryUpdated.next(void 0);
}
}
onRemove(ref, obj) {
var _a;
if (this.entries.has(ref)) {
this.entries.delete(ref);
if (obj === null || obj === void 0 ? void 0 : obj.data) {
this.clearHistoryForStructure(obj.data);
}
if (((_a = this.referenceLoci) === null || _a === void 0 ? void 0 : _a.structure) === (obj === null || obj === void 0 ? void 0 : obj.data)) {
this.referenceLoci = undefined;
}
this.state.stats = void 0;
this.events.changed.next(void 0);
}
}
onUpdate(ref, oldObj, obj) {
var _a, _b, _c, _d;
// no change to structure
if (oldObj === obj || (oldObj === null || oldObj === void 0 ? void 0 : oldObj.data) === obj.data)
return;
// ignore decorators to get stable ref
const cell = this.plugin.helpers.substructureParent.get(obj.data, true);
if (!cell)
return;
// only need to update the root
if (ref !== cell.transform.ref)
return;
if (!this.entries.has(ref))
return;
// use structure from last decorator as reference
const structure = (_b = (_a = this.plugin.helpers.substructureParent.get(obj.data)) === null || _a === void 0 ? void 0 : _a.obj) === null || _b === void 0 ? void 0 : _b.data;
if (!structure)
return;
// oldObj is not defined for inserts (e.g. TransformStructureConformation)
if (!(oldObj === null || oldObj === void 0 ? void 0 : oldObj.data) || Structure.areUnitIdsAndIndicesEqual(oldObj.data, obj.data)) {
this.entries.set(ref, remapSelectionEntry(this.entries.get(ref), structure));
// remap referenceLoci & prevHighlight if needed and possible
if (((_c = this.referenceLoci) === null || _c === void 0 ? void 0 : _c.structure.root) === structure.root) {
this.referenceLoci = StructureElement.Loci.remap(this.referenceLoci, structure);
}
// remap history locis if needed and possible
let changedHistory = false;
for (const e of this.state.additionsHistory) {
if (e.loci.structure.root === structure.root) {
e.loci = StructureElement.Loci.remap(e.loci, structure);
changedHistory = true;
}
}
if (changedHistory)
this.events.additionsHistoryUpdated.next(void 0);
}
else {
// clear the selection for ref
this.entries.set(ref, new SelectionEntry(StructureElement.Loci(structure, [])));
if (((_d = this.referenceLoci) === null || _d === void 0 ? void 0 : _d.structure.root) === structure.root) {
this.referenceLoci = undefined;
}
this.clearHistoryForStructure(structure);
this.state.stats = void 0;
this.events.changed.next(void 0);
}
}
/** Removes all selections and returns them */
clear() {
const keys = this.entries.keys();
const selections = [];
while (true) {
const k = keys.next();
if (k.done)
break;
const s = this.entries.get(k.value);
if (!StructureElement.Loci.isEmpty(s.selection))
selections.push(s.selection);
s.selection = StructureElement.Loci(s.selection.structure, []);
}
this.referenceLoci = undefined;
this.state.stats = void 0;
this.events.changed.next(void 0);
this.events.loci.clear.next(void 0);
this.clearHistory();
return selections;
}
getLoci(structure) {
const entry = this.getEntry(structure);
if (!entry)
return EmptyLoci;
return entry.selection;
}
getStructure(structure) {
const entry = this.getEntry(structure);
if (!entry)
return;
return entry.structure;
}
structureHasSelection(structure) {
var _a, _b;
const s = (_b = (_a = structure.cell) === null || _a === void 0 ? void 0 : _a.obj) === null || _b === void 0 ? void 0 : _b.data;
if (!s)
return false;
const entry = this.getEntry(s);
return !!entry && !StructureElement.Loci.isEmpty(entry.selection);
}
has(loci) {
if (StructureElement.Loci.is(loci)) {
const entry = this.getEntry(loci.structure);
if (entry) {
return StructureElement.Loci.isSubset(entry.selection, loci);
}
}
return false;
}
tryGetRange(loci) {
if (!StructureElement.Loci.is(loci))
return;
if (!this.getEntry(loci.structure))
return;
return getLociRange(this.referenceLoci, loci);
}
/** Count of all selected elements */
elementCount() {
let count = 0;
this.entries.forEach(v => {
count += StructureElement.Loci.size(v.selection);
});
return count;
}
getBoundary() {
const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
boundaryHelper.reset();
const boundaries = [];
this.entries.forEach(v => {
const loci = v.selection;
if (!StructureElement.Loci.isEmpty(loci)) {
boundaries.push(StructureElement.Loci.getBoundary(loci));
}
});
for (let i = 0, il = boundaries.length; i < il; ++i) {
const { box, sphere } = boundaries[i];
Vec3.min(min, min, box.min);
Vec3.max(max, max, box.max);
boundaryHelper.includePositionRadius(sphere.center, sphere.radius);
}
boundaryHelper.finishedIncludeStep();
for (let i = 0, il = boundaries.length; i < il; ++i) {
const { sphere } = boundaries[i];
boundaryHelper.radiusPositionRadius(sphere.center, sphere.radius);
}
return { box: { min, max }, sphere: boundaryHelper.getSphere() };
}
getPrincipalAxes() {
const values = iterableToArray(this.entries.values());
return StructureElement.Loci.getPrincipalAxesMany(values.map(v => v.selection));
}
modify(modifier, loci) {
let changed = false;
switch (modifier) {
case 'add':
changed = this.add(loci);
break;
case 'remove':
changed = this.remove(loci);
break;
case 'intersect':
changed = this.intersect(loci);
break;
case 'set':
changed = this.set(loci);
break;
}
if (changed) {
this.state.stats = void 0;
this.events.changed.next(void 0);
}
}
get applicableStructures() {
return this.plugin.managers.structure.hierarchy.selection.structures
.filter(s => !!s.cell.obj)
.map(s => s.cell.obj.data);
}
triggerInteraction(modifier, loci, applyGranularity = true) {
switch (modifier) {
case 'add':
this.plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
break;
case 'remove':
this.plugin.managers.interactivity.lociSelects.deselect({ loci }, applyGranularity);
break;
case 'intersect':
this.plugin.managers.interactivity.lociSelects.selectJoin({ loci }, applyGranularity);
break;
case 'set':
this.plugin.managers.interactivity.lociSelects.selectOnly({ loci }, applyGranularity);
break;
}
}
fromLoci(modifier, loci, applyGranularity = true) {
this.triggerInteraction(modifier, loci, applyGranularity);
}
fromCompiledQuery(modifier, query, applyGranularity = true) {
for (const s of this.applicableStructures) {
const loci = query(new QueryContext(s));
this.triggerInteraction(modifier, StructureSelection.toLociWithSourceUnits(loci), applyGranularity);
}
}
fromSelectionQuery(modifier, query, applyGranularity = true) {
this.plugin.runTask(Task.create('Structure Selection', async (runtime) => {
for (const s of this.applicableStructures) {
const loci = await query.getSelection(this.plugin, runtime, s);
this.triggerInteraction(modifier, StructureSelection.toLociWithSourceUnits(loci), applyGranularity);
}
}));
}
fromSelections(ref) {
var _a;
const cell = StateObjectRef.resolveAndCheck(this.plugin.state.data, ref);
if (!cell || !cell.obj)
return;
if (!PSO.Molecule.Structure.Selections.is(cell.obj)) {
console.warn('fromSelections applied to wrong object type.', cell.obj);
return;
}
this.clear();
for (const s of (_a = cell.obj) === null || _a === void 0 ? void 0 : _a.data) {
this.fromLoci('set', s.loci);
}
}
getSnapshot() {
const entries = [];
this.entries.forEach((entry, ref) => {
entries.push({
ref,
bundle: StructureElement.Bundle.fromLoci(entry.selection)
});
});
return { entries };
}
setSnapshot(snapshot) {
var _a, _b;
this.entries.clear();
for (const { ref, bundle } of snapshot.entries) {
const structure = (_b = (_a = this.plugin.state.data.select(StateSelection.Generators.byRef(ref))[0]) === null || _a === void 0 ? void 0 : _a.obj) === null || _b === void 0 ? void 0 : _b.data;
if (!structure)
continue;
const loci = StructureElement.Bundle.toLoci(bundle, structure);
this.fromLoci('set', loci, false);
}
}
constructor(plugin) {
super({ entries: new Map(), additionsHistory: [], stats: SelectionStats() });
this.plugin = plugin;
this.events = {
changed: this.ev(),
additionsHistoryUpdated: this.ev(),
loci: {
add: this.ev(),
remove: this.ev(),
clear: this.ev()
}
};
// listen to events from substructureParent helper to ensure it is updated
plugin.helpers.substructureParent.events.removed.subscribe(e => this.onRemove(e.ref, e.obj));
plugin.helpers.substructureParent.events.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
}
}
function SelectionStats() { return { structureCount: 0, elementCount: 0, label: 'Nothing Selected' }; }
;
class SelectionEntry {
get selection() { return this._selection; }
set selection(value) {
this._selection = value;
this._structure = void 0;
}
get structure() {
if (this._structure)
return this._structure;
if (Loci.isEmpty(this._selection)) {
this._structure = void 0;
}
else {
this._structure = StructureElement.Loci.toStructure(this._selection);
}
return this._structure;
}
constructor(selection) {
this._structure = void 0;
this._selection = selection;
}
}
/** remap `selection-entry` to be related to `structure` if possible */
function remapSelectionEntry(e, s) {
return new SelectionEntry(StructureElement.Loci.remap(e.selection, s));
}
/** Return loci spanning the range between `fromLoci` and `toLoci` (including both) if they belong to the same unit in the same structure */
export function getLociRange(fromLoci, toLoci) {
if (!StructureElement.Loci.is(fromLoci))
return;
if (!StructureElement.Loci.is(toLoci))
return;
if (fromLoci.structure !== toLoci.structure)
return;
if (toLoci.elements.length !== 1)
return;
const xs = toLoci.elements[0];
if (!xs)
return;
let e;
for (const _e of fromLoci.elements) {
if (xs.unit === _e.unit) {
e = _e;
break;
}
}
if (!e)
return;
if (xs.unit !== e.unit)
return;
return getElementRange(toLoci.structure, e, xs);
}
/**
* Assumes `ref` and `ext` belong to the same unit in the same structure
*/
function getElementRange(structure, ref, ext) {
const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices));
const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices));
return StructureElement.Loci(structure, [{
unit: ref.unit,
indices: OrderedSet.ofRange(min, max)
}]);
}