Selaa lähdekoodia

mol-plugin: support latest loci in selection manager

David Sehnal 5 vuotta sitten
vanhempi
commit
9f6e98a5d0

+ 8 - 0
src/mol-plugin/skin/base/components/controls.scss

@@ -226,6 +226,7 @@
 
 // TODO : get rid of the important
 .msp-control-group-header {
+    background: $default-background;
     > button {
         padding-left: $control-spacing / 2 !important;
         text-align: left !important;
@@ -235,6 +236,13 @@
         background: $default-background !important;
         color: color-lower-contrast($font-color, 15%) !important;
     }
+    > span {
+        padding-left: $control-spacing / 2;
+        line-height: 2 * $row-height / 3;
+        font-size: 70%;
+        background: $default-background;
+        color: color-lower-contrast($font-color, 15%);
+    }
 }
 
 .msp-control-group-footer {

+ 63 - 22
src/mol-plugin/ui/structure/selection.tsx

@@ -13,6 +13,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Interactivity } from '../../util/interactivity';
 import { ParameterControls } from '../controls/parameters';
 import { stripTags } from '../../../mol-util/string';
+import { StructureElement } from '../../../mol-model/structure';
 
 const SSQ = StructureSelectionQueries
 const DefaultQueries: (keyof typeof SSQ)[] = [
@@ -64,6 +65,18 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         this.plugin.canvas3d.camera.focus(center, radius, durationMs, normVecA, normVecC);
     }
 
+    focusSingle(loci: StructureElement.Loci) {
+        return () => {
+            const { extraRadius, minRadius, durationMs } = this.state
+            if (this.plugin.helpers.structureSelectionManager.stats.elementCount === 0) return
+            const { sphere } = StructureElement.Loci.getBoundary(loci);
+            const radius = Math.max(sphere.radius + extraRadius, minRadius);
+            // const principalAxes = this.plugin.helpers.structureSelectionManager.getPrincipalAxes();
+            // const { center, normVecA, normVecC } = principalAxes
+            this.plugin.canvas3d.camera.focus(sphere.center, radius, durationMs);
+        }
+    }
+
     setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
         if (p.name === 'granularity') {
             PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { granularity: p.value } });
@@ -85,6 +98,30 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
     remove = (value: string) => this.set('remove', value)
     only = (value: string) => this.set('only', value)
 
+    queries = Options(Object.keys(StructureSelectionQueries)
+            .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string])
+            .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ)));
+
+    controls = <div className='msp-control-row'>
+        <div className='msp-select-row'>
+            <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}>
+                <optgroup label='Select'>
+                    {this.queries}
+                </optgroup>
+            </ButtonSelect>
+            <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}>
+                <optgroup label='Deselect'>
+                    {this.queries}
+                </optgroup>
+            </ButtonSelect>
+            <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}>
+                <optgroup label='Only'>
+                    {this.queries}
+                </optgroup>
+            </ButtonSelect>
+        </div>
+    </div>
+
     defaultState() {
         return {
             isCollapsed: false,
@@ -99,9 +136,24 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
     }
 
     renderControls() {
-        const queries = Object.keys(StructureSelectionQueries)
-            .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string])
-            .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ))
+        const latest: JSX.Element[] = [];
+
+        const mng = this.plugin.helpers.structureSelectionManager;
+
+        // TODO: fix the styles, move them to CSS
+
+        for (let i = 0, _i = Math.min(3, mng.latestLoci.length); i < _i; i++) {
+            const e = mng.latestLoci[i];
+            latest.push(<li key={e!.label}>
+                <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
+                    title='Click to focus.' onClick={this.focusSingle(e.loci)}>
+                    <span dangerouslySetInnerHTML={{ __html: e.label }} />
+                </button>
+                {/* <div>
+                    <IconButton icon='remove' title='Remove' onClick={() => {}} />
+                </div> */}
+            </li>)
+        }
 
         return <div>
             <div className='msp-control-row msp-row-text'>
@@ -111,25 +163,14 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
                 </button>
             </div>
             <ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
-            <div className='msp-control-row'>
-                <div className='msp-select-row'>
-                    <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}>
-                        <optgroup label='Select'>
-                            {Options(queries)}
-                        </optgroup>
-                    </ButtonSelect>
-                    <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}>
-                        <optgroup label='Deselect'>
-                            {Options(queries)}
-                        </optgroup>
-                    </ButtonSelect>
-                    <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}>
-                        <optgroup label='Only'>
-                            {Options(queries)}
-                        </optgroup>
-                    </ButtonSelect>
-                </div>
-            </div>
+            {this.controls}
+            { latest.length > 0 &&
+            <>
+                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Latest Selections</span></div>
+                <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
+                    {latest}
+                </ul>
+            </>}
         </div>
     }
 }

+ 68 - 3
src/mol-plugin/util/structure-element-selection.ts

@@ -17,12 +17,15 @@ 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 Matrix from '../../mol-math/linear-algebra/matrix/matrix';
+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 getEntry(s: Structure) {
         const cell = this.plugin.helpers.substructureParent.get(s);
@@ -37,6 +40,10 @@ class StructureElementSelectionManager {
         return this.entries.get(ref)!;
     }
 
+    get latestLoci(): ReadonlyArray<LatestEntry> {
+        return this._latestLoci;
+    }
+
     /** Count of all selected elements */
     size() {
         let count = 0
@@ -114,7 +121,8 @@ class StructureElementSelectionManager {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 entry.selection = StructureElement.Loci.union(entry.selection, loci);
-                this.plugin.events.interactivity.selectionUpdated.next()
+                this.addLatest(loci);
+                this.plugin.events.interactivity.selectionUpdated.next();
                 return entry.selection;
             }
         }
@@ -126,7 +134,8 @@ class StructureElementSelectionManager {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
-                this.plugin.events.interactivity.selectionUpdated.next()
+                this.removeLatest(loci);
+                this.plugin.events.interactivity.selectionUpdated.next();
                 return StructureElement.Loci.isEmpty(entry.selection) ? EmptyLoci : entry.selection;
             }
         }
@@ -216,7 +225,11 @@ class StructureElementSelectionManager {
     }
 
     private onRemove(ref: string) {
-        if (this.entries.has(ref)) this.entries.delete(ref);
+        if (this.entries.has(ref)) {
+            this.entries.delete(ref);
+            // TODO: property update the latest loci
+            this._latestLoci = [];
+        }
     }
 
     private onUpdate(ref: string, oldObj: StateObject | undefined, obj: StateObject) {
@@ -225,6 +238,9 @@ class StructureElementSelectionManager {
         if (this.entries.has(ref)) {
             if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
 
+            // TODO: property update the latest loci
+            this._latestLoci = [];
+
             // 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));
@@ -236,6 +252,50 @@ class StructureElementSelectionManager {
         }
     }
 
+    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));
@@ -246,6 +306,11 @@ interface SelectionEntry {
     selection: StructureElement.Loci
 }
 
+interface LatestEntry {
+    loci: StructureElement.Loci,
+    label: string
+}
+
 function SelectionEntry(s: Structure): SelectionEntry {
     return {
         selection: StructureElement.Loci(s, [])

+ 1 - 0
src/mol-plugin/util/structure-selection-helper.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';

+ 7 - 4
src/mol-util/array.ts

@@ -78,12 +78,15 @@ export function arrayRemoveInPlace<T>(xs: T[], x: T) {
         }
     }
     if (!found) return false;
-    i++;
-    for (; i < l; i++) {
-        xs[i - 1] = xs[i];
+    arrayRemoveAtInPlace(xs, i);
+    return true;
+}
+
+export function arrayRemoveAtInPlace<T>(xs: T[], idx: number) {
+    for (let i = idx, _i = xs.length - 1; i < _i; i++) {
+        xs[i] = xs[i + 1];
     }
     xs.pop();
-    return true;
 }
 
 export function arraySetAdd<T>(xs: T[], x: T) {