Browse Source

wip, measurements ui

Alexander Rose 5 years ago
parent
commit
ba1509b37a

+ 3 - 1
src/mol-plugin-ui/controls.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 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>
@@ -18,6 +18,7 @@ import { ModelFromTrajectory } from '../mol-plugin-state/transforms/model';
 import { AnimationControls } from './state/animation';
 import { StructureRepresentationControls } from './structure/representation';
 import { StructureSelectionControls } from './structure/selection';
+import { StructureMeasurementsControls } from './structure/measurements';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
     state = { show: false, label: '' }
@@ -266,6 +267,7 @@ export class StructureToolsWrapper extends PluginUIComponent {
 
             <StructureSelectionControls />
             <StructureRepresentationControls />
+            <StructureMeasurementsControls />
         </div>;
     }
 }

+ 170 - 0
src/mol-plugin-ui/structure/measurements.tsx

@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import { CollapsableControls, CollapsableState } from '../base';
+import { lociLabel } from '../../mol-theme/label';
+import { StructureElement } from '../../mol-model/structure';
+
+// TODO hide/show, delete, details, options (e.g. change text for labels)
+// TODO better labels: shorter, include measure
+// TODO better updates on state changes
+
+interface StructureMeasurementsControlsState extends CollapsableState {
+    minRadius: number,
+    extraRadius: number,
+    durationMs: number,
+
+    isDisabled: boolean,
+}
+
+export class StructureMeasurementsControls<P, S extends StructureMeasurementsControlsState> extends CollapsableControls<P, S> {
+    componentDidMount() {
+        this.subscribe(this.plugin.events.state.object.updated, ({ }) => {
+            // TODO
+            this.forceUpdate()
+        })
+
+        this.subscribe(this.plugin.events.state.object.created, ({ }) => {
+            // TODO
+            this.forceUpdate()
+        })
+
+        this.subscribe(this.plugin.events.state.object.removed, ({ }) => {
+            // TODO
+            this.forceUpdate()
+        })
+
+        this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
+            this.setState({ isDisabled: v })
+        })
+    }
+
+    focusLoci(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);
+            this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
+        }
+    }
+
+    defaultState() {
+        return {
+            isCollapsed: false,
+            header: 'Measurements',
+
+            minRadius: 8,
+            extraRadius: 4,
+            durationMs: 250,
+
+            isDisabled: false
+        } as S
+    }
+
+    renderControls() {
+        const labels: JSX.Element[] = [];
+        const distances: JSX.Element[] = [];
+        const angles: JSX.Element[] = [];
+        const dihedrals: JSX.Element[] = [];
+
+        const measurements = this.plugin.helpers.measurement.getMeasurements();
+
+        for (const d of measurements.labels) {
+            const source = d.obj?.data.source
+            if (source) {
+                const lA = simpleLabel(source.data[0].loci)
+                labels.push(<li key={source.id}>
+                    <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
+                        title='Click to focus.' onClick={this.focusLoci(source.data[0].loci)}>
+                        <span dangerouslySetInnerHTML={{ __html: `${lA}` }} />
+                    </button>
+                </li>)
+            }
+        }
+
+        for (const d of measurements.distances) {
+            const source = d.obj?.data.source
+            if (source) {
+                const lA = simpleLabel(source.data[0].loci)
+                const lB = simpleLabel(source.data[1].loci)
+                distances.push(<li key={source.id}>
+                    <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
+                        title='Click to focus.' onClick={this.focusLoci(source.data[0].loci)}>
+                        <span dangerouslySetInnerHTML={{ __html: `${lA} \u2014 ${lB}` }} />
+                    </button>
+                </li>)
+            }
+        }
+
+        for (const d of measurements.angles) {
+            const source = d.obj?.data.source
+            if (source) {
+                const lA = simpleLabel(source.data[0].loci)
+                const lB = simpleLabel(source.data[1].loci)
+                const lC = simpleLabel(source.data[2].loci)
+                angles.push(<li key={source.id}>
+                    <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
+                        title='Click to focus.' onClick={this.focusLoci(source.data[0].loci)}>
+                        <span dangerouslySetInnerHTML={{ __html: `${lA} \u2014 ${lB} \u2014 ${lC}` }} />
+                    </button>
+                </li>)
+            }
+        }
+
+        for (const d of measurements.dihedrals) {
+            const source = d.obj?.data.source
+            if (source) {
+                const lA = simpleLabel(source.data[0].loci)
+                const lB = simpleLabel(source.data[1].loci)
+                const lC = simpleLabel(source.data[2].loci)
+                const lD = simpleLabel(source.data[3].loci)
+                dihedrals.push(<li key={source.id}>
+                    <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
+                        title='Click to focus.' onClick={this.focusLoci(source.data[0].loci)}>
+                        <span dangerouslySetInnerHTML={{ __html: `${lA} \u2014 ${lB} \u2014 ${lC} \u2014 ${lD}` }} />
+                    </button>
+                </li>)
+            }
+        }
+
+        return <div>
+            {labels.length > 0 && <div>
+                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Labels</span></div>
+                <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
+                    {labels}
+                </ul>
+            </div>}
+            {distances.length > 0 && <div>
+                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Distances</span></div>
+                <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
+                    {distances}
+                </ul>
+            </div>}
+            {angles.length > 0 && <div>
+                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Angles</span></div>
+                <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
+                    {angles}
+                </ul>
+            </div>}
+            {dihedrals.length > 0 && <div>
+                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Dihedrals</span></div>
+                <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
+                    {dihedrals}
+                </ul>
+            </div>}
+        </div>
+    }
+}
+
+function simpleLabel(loci: StructureElement.Loci) {
+    return lociLabel(loci, { htmlStyling: false })
+        .split('|')
+        .reverse()[0]
+        .replace(/\[.*\]/g, '')
+        .trim()
+}

+ 1 - 1
src/mol-plugin-ui/structure/selection.tsx

@@ -180,7 +180,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             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.focusLoci(e.loci)}>
-                    <span dangerouslySetInnerHTML={{ __html: e.label }} />
+                    <span dangerouslySetInnerHTML={{ __html: e.label.split('|').reverse().join(' | ') }} />
                 </button>
                 {/* <div>
                     <IconButton icon='remove' title='Remove' onClick={() => {}} />

+ 33 - 2
src/mol-plugin/util/structure-measurement.ts

@@ -6,14 +6,14 @@
 
 import { StructureElement } from '../../mol-model/structure';
 import { PluginContext } from '../context';
-import { StateSelection, StateTransform } from '../../mol-state';
+import { StateSelection, StateTransform, StateTransformer } from '../../mol-state';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
 import { PluginCommands } from '../commands';
 import { arraySetAdd } from '../../mol-util/array';
 
 export { StructureMeasurementManager }
 
-const MeasurementGroupTag = 'measurement-group';
+export const MeasurementGroupTag = 'measurement-group';
 
 class StructureMeasurementManager {
     private getGroup() {
@@ -25,6 +25,37 @@ class StructureMeasurementManager {
         return builder.toRoot().group(StateTransforms.Misc.CreateGroup, { label: `Measurements` }, { tags: MeasurementGroupTag });
     }
 
+    private getTransforms(transformer: StateTransformer) {
+        const state = this.context.state.dataState;
+        const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
+        return groupRef ? state.select(StateSelection.Generators.ofTransformer(transformer, groupRef)) : []
+    }
+
+    getLabels() {
+        return this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D)
+    }
+
+    getDistances() {
+        return this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D)
+    }
+
+    getAngles() {
+        return this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D)
+    }
+
+    getDihedrals() {
+        return this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D)
+    }
+
+    getMeasurements() {
+        return {
+            labels: this.getLabels(),
+            distances: this.getDistances(),
+            angles: this.getAngles(),
+            dihedrals: this.getDihedrals(),
+        }
+    }
+
     async addDistance(a: StructureElement.Loci, b: StructureElement.Loci) {
         const cellA = this.context.helpers.substructureParent.get(a.structure);
         const cellB = this.context.helpers.substructureParent.get(b.structure);