|
@@ -1,350 +0,0 @@
|
|
|
-/**
|
|
|
- * Copyright (c) 2019 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 { OrderedSet } from '../../mol-data/int';
|
|
|
-import { EmptyLoci, Loci } from '../../mol-model/loci';
|
|
|
-import { Structure, StructureElement } from '../../mol-model/structure';
|
|
|
-import { StateObject } from '../../mol-state';
|
|
|
-import { PluginContext } from '../context';
|
|
|
-import { PluginStateObject } from '../../mol-plugin-state/objects';
|
|
|
-import { structureElementStatsLabel } from '../../mol-theme/label';
|
|
|
-import { Vec3 } from '../../mol-math/linear-algebra';
|
|
|
-import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
|
|
-import { Boundary } from '../../mol-model/structure/structure/util/boundary';
|
|
|
-import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
|
|
|
-import { arrayRemoveAtInPlace } from '../../mol-util/array';
|
|
|
-
|
|
|
-const boundaryHelper = new BoundaryHelper();
|
|
|
-const LATEST_LOCI_CAPACITY = 8;
|
|
|
-
|
|
|
-export { StructureElementSelectionManager };
|
|
|
-class StructureElementSelectionManager {
|
|
|
- private entries = new Map<string, SelectionEntry>();
|
|
|
- private _latestLoci: LatestEntry[] = [];
|
|
|
- private referenceLoci: Loci | undefined
|
|
|
-
|
|
|
- private getEntry(s: Structure) {
|
|
|
- const cell = this.plugin.helpers.substructureParent.get(s);
|
|
|
- if (!cell) return;
|
|
|
- const ref = cell.transform.ref;
|
|
|
- if (!this.entries.has(ref)) {
|
|
|
- const entry = SelectionEntry(s);
|
|
|
- this.entries.set(ref, entry);
|
|
|
- return entry;
|
|
|
- }
|
|
|
-
|
|
|
- return this.entries.get(ref)!;
|
|
|
- }
|
|
|
-
|
|
|
- get latestLoci(): ReadonlyArray<LatestEntry> {
|
|
|
- return this._latestLoci;
|
|
|
- }
|
|
|
-
|
|
|
- /** Count of all selected elements */
|
|
|
- size() {
|
|
|
- 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(0);
|
|
|
-
|
|
|
- const boundaries: Boundary[] = []
|
|
|
- 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.boundaryStep(sphere.center, sphere.radius)
|
|
|
- }
|
|
|
-
|
|
|
- boundaryHelper.finishBoundaryStep();
|
|
|
-
|
|
|
- for (let i = 0, il = boundaries.length; i < il; ++i) {
|
|
|
- const { sphere } = boundaries[i];
|
|
|
- boundaryHelper.extendStep(sphere.center, sphere.radius);
|
|
|
- }
|
|
|
-
|
|
|
- return { box: { min, max }, sphere: boundaryHelper.getSphere() };
|
|
|
- }
|
|
|
-
|
|
|
- getPrincipalAxes(): PrincipalAxes {
|
|
|
- const elementCount = this.size()
|
|
|
- const positions = new Float32Array(3 * elementCount)
|
|
|
- let offset = 0
|
|
|
- this.entries.forEach(v => {
|
|
|
- StructureElement.Loci.toPositionsArray(v.selection, positions, offset)
|
|
|
- offset += StructureElement.Loci.size(v.selection) * 3
|
|
|
- })
|
|
|
- return PrincipalAxes.ofPositions(positions)
|
|
|
- }
|
|
|
-
|
|
|
- get stats() {
|
|
|
- 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: Loci): Loci {
|
|
|
- if (StructureElement.Loci.is(loci)) {
|
|
|
- const entry = this.getEntry(loci.structure);
|
|
|
- if (entry) {
|
|
|
- entry.selection = StructureElement.Loci.union(entry.selection, loci);
|
|
|
- this.addLatest(loci);
|
|
|
- this.referenceLoci = loci
|
|
|
- this.plugin.events.interactivity.selectionUpdated.next();
|
|
|
- return entry.selection;
|
|
|
- }
|
|
|
- }
|
|
|
- return EmptyLoci
|
|
|
- }
|
|
|
-
|
|
|
- remove(loci: Loci): Loci {
|
|
|
- if (StructureElement.Loci.is(loci)) {
|
|
|
- const entry = this.getEntry(loci.structure);
|
|
|
- if (entry) {
|
|
|
- entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
|
|
|
- this.removeLatest(loci);
|
|
|
- this.referenceLoci = loci
|
|
|
- this.plugin.events.interactivity.selectionUpdated.next();
|
|
|
- return StructureElement.Loci.isEmpty(entry.selection) ? EmptyLoci : entry.selection;
|
|
|
- }
|
|
|
- }
|
|
|
- return EmptyLoci
|
|
|
- }
|
|
|
-
|
|
|
- set(loci: Loci): Loci {
|
|
|
- if (StructureElement.Loci.is(loci)) {
|
|
|
- const entry = this.getEntry(loci.structure);
|
|
|
- if (entry) {
|
|
|
- entry.selection = loci;
|
|
|
- this.referenceLoci = undefined
|
|
|
- this.plugin.events.interactivity.selectionUpdated.next()
|
|
|
- return StructureElement.Loci.isEmpty(entry.selection) ? EmptyLoci : entry.selection;
|
|
|
- }
|
|
|
- }
|
|
|
- return EmptyLoci;
|
|
|
- }
|
|
|
-
|
|
|
- /** Removes all selections and returns them */
|
|
|
- clear() {
|
|
|
- const keys = this.entries.keys();
|
|
|
- const selections: StructureElement.Loci[] = [];
|
|
|
- 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.plugin.events.interactivity.selectionUpdated.next()
|
|
|
- return selections;
|
|
|
- }
|
|
|
-
|
|
|
- get(structure: Structure) {
|
|
|
- const entry = this.getEntry(structure);
|
|
|
- if (!entry) return EmptyLoci;
|
|
|
- return entry.selection;
|
|
|
- }
|
|
|
-
|
|
|
- has(loci: 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: Loci): StructureElement.Loci | undefined {
|
|
|
- if (!StructureElement.Loci.is(loci)) return;
|
|
|
- if (loci.elements.length !== 1) return;
|
|
|
- const entry = this.getEntry(loci.structure);
|
|
|
- if (!entry) return;
|
|
|
-
|
|
|
- const xs = loci.elements[0];
|
|
|
- if (!xs) return;
|
|
|
-
|
|
|
- const ref = this.referenceLoci
|
|
|
- if (!ref || !StructureElement.Loci.is(ref) || ref.structure.root !== loci.structure.root) return;
|
|
|
-
|
|
|
- let e: StructureElement.Loci['elements'][0] | undefined;
|
|
|
- for (const _e of ref.elements) {
|
|
|
- if (xs.unit === _e.unit) {
|
|
|
- e = _e;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (!e) return;
|
|
|
-
|
|
|
- if (xs.unit !== e.unit) return;
|
|
|
-
|
|
|
- return getElementRange(loci.structure.root, e, xs)
|
|
|
- }
|
|
|
-
|
|
|
- private prevHighlight: StructureElement.Loci | undefined = void 0;
|
|
|
-
|
|
|
- accumulateInteractiveHighlight(loci: Loci) {
|
|
|
- if (StructureElement.Loci.is(loci)) {
|
|
|
- if (this.prevHighlight) {
|
|
|
- this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
|
|
|
- } else {
|
|
|
- this.prevHighlight = loci;
|
|
|
- }
|
|
|
- }
|
|
|
- return this.prevHighlight;
|
|
|
- }
|
|
|
-
|
|
|
- clearInteractiveHighlight() {
|
|
|
- const ret = this.prevHighlight;
|
|
|
- this.prevHighlight = void 0;
|
|
|
- return ret || EmptyLoci;
|
|
|
- }
|
|
|
-
|
|
|
- private onRemove(ref: string) {
|
|
|
- if (this.entries.has(ref)) {
|
|
|
- this.entries.delete(ref);
|
|
|
- // TODO: property update the latest loci
|
|
|
- this._latestLoci = [];
|
|
|
- this.referenceLoci = undefined
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private onUpdate(ref: string, oldObj: StateObject | undefined, obj: StateObject) {
|
|
|
- if (!PluginStateObject.Molecule.Structure.is(obj)) return;
|
|
|
-
|
|
|
- if (this.entries.has(ref)) {
|
|
|
- if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
|
|
|
-
|
|
|
- // TODO: property update the latest loci & reference loci
|
|
|
- this._latestLoci = [];
|
|
|
- this.referenceLoci = undefined
|
|
|
-
|
|
|
- // remap the old selection to be related to the new object if possible.
|
|
|
- if (Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
|
|
|
- this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, obj.data));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // clear the selection
|
|
|
- this.entries.set(ref, SelectionEntry(obj.data));
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private addLatest(loci: StructureElement.Loci) {
|
|
|
- if (Loci.isEmpty(loci)) return;
|
|
|
-
|
|
|
- let idx = 0, entry: LatestEntry | undefined = void 0;
|
|
|
- for (const l of this._latestLoci) {
|
|
|
- if (Loci.areEqual(l.loci, loci)) {
|
|
|
- entry = l;
|
|
|
- break;
|
|
|
- }
|
|
|
- idx++;
|
|
|
- }
|
|
|
-
|
|
|
- if (entry) {
|
|
|
- arrayRemoveAtInPlace(this._latestLoci, idx);
|
|
|
- this._latestLoci.unshift(entry);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const stats = StructureElement.Stats.ofLoci(loci);
|
|
|
- const label = structureElementStatsLabel(stats)
|
|
|
-
|
|
|
- this._latestLoci.unshift({ loci, label });
|
|
|
- if (this._latestLoci.length > LATEST_LOCI_CAPACITY) this._latestLoci.pop();
|
|
|
- }
|
|
|
-
|
|
|
- private removeLatest(loci: Loci) {
|
|
|
- if (Loci.isEmpty(loci)) return;
|
|
|
-
|
|
|
- let idx = 0, found = false;
|
|
|
- for (const l of this._latestLoci) {
|
|
|
- if (Loci.areEqual(l.loci, loci)) {
|
|
|
- found = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- idx++;
|
|
|
- }
|
|
|
-
|
|
|
- if (found) {
|
|
|
- arrayRemoveAtInPlace(this._latestLoci, idx);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // private removeLatestOnChange
|
|
|
-
|
|
|
- constructor(private plugin: PluginContext) {
|
|
|
- plugin.state.dataState.events.object.removed.subscribe(e => this.onRemove(e.ref));
|
|
|
- plugin.state.dataState.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-interface SelectionEntry {
|
|
|
- selection: StructureElement.Loci
|
|
|
-}
|
|
|
-
|
|
|
-interface LatestEntry {
|
|
|
- loci: StructureElement.Loci,
|
|
|
- label: string
|
|
|
-}
|
|
|
-
|
|
|
-function SelectionEntry(s: Structure): SelectionEntry {
|
|
|
- return {
|
|
|
- selection: StructureElement.Loci(s, [])
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-/** remap `selection-entry` to be related to `structure` if possible */
|
|
|
-function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry {
|
|
|
- return {
|
|
|
- selection: StructureElement.Loci.remap(e.selection, s)
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Assumes `ref` and `ext` belong to the same unit in the same structure
|
|
|
- */
|
|
|
-function getElementRange(structure: Structure, ref: StructureElement.Loci['elements'][0], ext: StructureElement.Loci['elements'][0]) {
|
|
|
- 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 as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
|
|
|
- }]);
|
|
|
-}
|