From ba1509b37ad6d4fae6502bb70e72d779aceccab3 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Fri, 6 Mar 2020 18:02:47 -0800 Subject: [PATCH] wip, measurements ui --- src/mol-plugin-ui/controls.tsx | 4 +- src/mol-plugin-ui/structure/measurements.tsx | 170 +++++++++++++++++++ src/mol-plugin-ui/structure/selection.tsx | 2 +- src/mol-plugin/util/structure-measurement.ts | 35 +++- 4 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 src/mol-plugin-ui/structure/measurements.tsx diff --git a/src/mol-plugin-ui/controls.tsx b/src/mol-plugin-ui/controls.tsx index 2e27e3dfe..1fffb09ab 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 000000000..1b5ef5cd6 --- /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 76225830a..88e6e4802 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 c8ee5865a..fc199b4c8 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); -- GitLab