diff --git a/src/mol-plugin-ui/controls.tsx b/src/mol-plugin-ui/controls.tsx
index 2e27e3dfee6bdc0e0da23a1b80705e6e57030ab4..1fffb09ab8c891e704d9c3cdbe1863529a112885 100644
--- a/src/mol-plugin-ui/controls.tsx
+++ b/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>;
}
}
diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1b5ef5cd68b8415fd21c3594860ec2c651c0887b
--- /dev/null
+++ b/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()
+}
\ No newline at end of file
diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx
index 76225830a77a2beaa8f6b56fed8d46b904e0b048..88e6e480242b122bbbc4fcc4bc6e25daab27c590 100644
--- a/src/mol-plugin-ui/structure/selection.tsx
+++ b/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={() => {}} />
diff --git a/src/mol-plugin/util/structure-measurement.ts b/src/mol-plugin/util/structure-measurement.ts
index c8ee5865a2299f044557b12689d064f8adc48dad..fc199b4c86d22636fb991bbfdd17424960c9d2f7 100644
--- a/src/mol-plugin/util/structure-measurement.ts
+++ b/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);