|
@@ -5,184 +5,175 @@
|
|
*/
|
|
*/
|
|
|
|
|
|
import * as React from 'react';
|
|
import * as React from 'react';
|
|
-import { CollapsableControls, CollapsableState } from '../base';
|
|
|
|
|
|
+import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
|
|
import { lociLabel, dihedralLabel, angleLabel, distanceLabel } from '../../mol-theme/label';
|
|
import { lociLabel, dihedralLabel, angleLabel, distanceLabel } from '../../mol-theme/label';
|
|
import { Loci } from '../../mol-model/loci';
|
|
import { Loci } from '../../mol-model/loci';
|
|
import { FiniteArray } from '../../mol-util/type-helpers';
|
|
import { FiniteArray } from '../../mol-util/type-helpers';
|
|
-import { StateObjectCell, StateTransform, StateTransformer } from '../../mol-state';
|
|
|
|
|
|
+import { State } from '../../mol-state';
|
|
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
|
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
|
-import { ShapeRepresentation } from '../../mol-repr/shape/representation';
|
|
|
|
-import { IconButton } from '../controls/common';
|
|
|
|
|
|
+import { IconButton, ExpandGroup } from '../controls/common';
|
|
import { PluginCommands } from '../../mol-plugin/commands';
|
|
import { PluginCommands } from '../../mol-plugin/commands';
|
|
|
|
+import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement';
|
|
|
|
+import { ParameterControls } from '../controls/parameters';
|
|
|
|
|
|
// TODO details, options (e.g. change text for labels)
|
|
// TODO details, options (e.g. change text for labels)
|
|
// TODO better updates on state changes
|
|
// TODO better updates on state changes
|
|
|
|
|
|
-type MeasurementTransform = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>>
|
|
|
|
-
|
|
|
|
|
|
+const MeasurementFocusOptions = {
|
|
|
|
+ minRadius: 8,
|
|
|
|
+ extraRadius: 4,
|
|
|
|
+ durationMs: 250,
|
|
|
|
+ unitLabel: '\u212B',
|
|
|
|
+}
|
|
|
|
|
|
interface StructureMeasurementsControlsState extends CollapsableState {
|
|
interface StructureMeasurementsControlsState extends CollapsableState {
|
|
- minRadius: number,
|
|
|
|
- extraRadius: number,
|
|
|
|
- durationMs: number,
|
|
|
|
unitLabel: string,
|
|
unitLabel: string,
|
|
|
|
|
|
isDisabled: boolean,
|
|
isDisabled: boolean,
|
|
}
|
|
}
|
|
|
|
|
|
-export class StructureMeasurementsControls<P, S extends StructureMeasurementsControlsState> extends CollapsableControls<P, S> {
|
|
|
|
|
|
+export class StructureMeasurementsControls extends CollapsableControls<{}, StructureMeasurementsControlsState> {
|
|
componentDidMount() {
|
|
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.managers.structure.measurement.behaviors.state, () => {
|
|
|
|
+ this.forceUpdate();
|
|
|
|
+ });
|
|
|
|
|
|
this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
|
|
this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
|
|
this.setState({ isDisabled: v })
|
|
this.setState({ isDisabled: v })
|
|
- })
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- focus(selections: PluginStateObject.Molecule.Structure.Selections) {
|
|
|
|
- return () => {
|
|
|
|
- const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data))
|
|
|
|
- if (sphere) {
|
|
|
|
- const { extraRadius, minRadius, durationMs } = this.state
|
|
|
|
- const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
|
|
|
- this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ });
|
|
}
|
|
}
|
|
|
|
|
|
defaultState() {
|
|
defaultState() {
|
|
return {
|
|
return {
|
|
isCollapsed: false,
|
|
isCollapsed: false,
|
|
header: 'Measurements',
|
|
header: 'Measurements',
|
|
-
|
|
|
|
- minRadius: 8,
|
|
|
|
- extraRadius: 4,
|
|
|
|
- durationMs: 250,
|
|
|
|
unitLabel: '\u212B',
|
|
unitLabel: '\u212B',
|
|
-
|
|
|
|
isDisabled: false
|
|
isDisabled: false
|
|
- } as S
|
|
|
|
|
|
+ } as StructureMeasurementsControlsState
|
|
}
|
|
}
|
|
|
|
|
|
- getLabel(selections: PluginStateObject.Molecule.Structure.Selections) {
|
|
|
|
- switch (selections.data.length) {
|
|
|
|
- case 1: return lociLabel(selections.data[0].loci, { condensed: true })
|
|
|
|
- case 2: return distanceLabel(toLociBundle(selections.data), { condensed: true })
|
|
|
|
- case 3: return angleLabel(toLociBundle(selections.data), { condensed: true })
|
|
|
|
- case 4: return dihedralLabel(toLociBundle(selections.data), { condensed: true })
|
|
|
|
|
|
+ renderGroup(cells: ReadonlyArray<StructureMeasurementCell>, header: string) {
|
|
|
|
+ const group: JSX.Element[] = [];
|
|
|
|
+ for (const cell of cells) {
|
|
|
|
+ if (cell.obj) group.push(<MeasurementEntry key={cell.obj.id} cell={cell} />)
|
|
}
|
|
}
|
|
- return ''
|
|
|
|
|
|
+ return group.length ? <ExpandGroup header={header} initiallyExpanded={true}>{group}</ExpandGroup> : null;
|
|
}
|
|
}
|
|
|
|
|
|
- highlight(repr: ShapeRepresentation<any, any, any>, selections: PluginStateObject.Molecule.Structure.Selections) {
|
|
|
|
- return () => {
|
|
|
|
- this.plugin.interactivity.lociHighlights.clearHighlights();
|
|
|
|
- for (const d of selections.data) {
|
|
|
|
- this.plugin.interactivity.lociHighlights.highlight({ loci: d.loci }, false);
|
|
|
|
- }
|
|
|
|
- this.plugin.interactivity.lociHighlights.highlight({ loci: repr.getLoci() }, false);
|
|
|
|
- }
|
|
|
|
|
|
+ renderControls() {
|
|
|
|
+ const measurements = this.plugin.managers.structure.measurement.state;
|
|
|
|
+
|
|
|
|
+ return <>
|
|
|
|
+ {this.renderGroup(measurements.labels, 'Labels')}
|
|
|
|
+ {this.renderGroup(measurements.distances, 'Distances')}
|
|
|
|
+ {this.renderGroup(measurements.angles, 'Angles')}
|
|
|
|
+ {this.renderGroup(measurements.dihedrals, 'Dihedrals')}
|
|
|
|
+ <MeasurementsOptions />
|
|
|
|
+ </>
|
|
}
|
|
}
|
|
|
|
+}
|
|
|
|
|
|
- clearHighlight() {
|
|
|
|
- return () => {
|
|
|
|
- this.plugin.interactivity.lociHighlights.clearHighlights();
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+export class MeasurementsOptions extends PurePluginUIComponent<{}, { isDisabled: boolean }> {
|
|
|
|
+ state = { isDisabled: false }
|
|
|
|
|
|
- delete(cell: MeasurementTransform) {
|
|
|
|
- return () => {
|
|
|
|
- PluginCommands.State.RemoveObject(this.plugin, { state: cell.parent, ref: cell.transform.parent, removeParentGhosts: true });
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ componentDidMount() {
|
|
|
|
+ this.subscribe(this.plugin.managers.structure.measurement.behaviors.state, () => {
|
|
|
|
+ this.forceUpdate();
|
|
|
|
+ });
|
|
|
|
|
|
- toggleVisibility(cell: MeasurementTransform) {
|
|
|
|
- return (e: React.MouseEvent<HTMLElement>) => {
|
|
|
|
- e.preventDefault();
|
|
|
|
- PluginCommands.State.ToggleVisibility(this.plugin, { state: cell.parent, ref: cell.transform.parent });
|
|
|
|
- e.currentTarget.blur();
|
|
|
|
- }
|
|
|
|
|
|
+ this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
|
|
|
|
+ this.setState({ isDisabled: v })
|
|
|
|
+ });
|
|
}
|
|
}
|
|
|
|
|
|
- getRow(cell: MeasurementTransform) {
|
|
|
|
- const { obj } = cell
|
|
|
|
- if (!obj) return null
|
|
|
|
|
|
+ changed = (options: StructureMeasurementOptions) => {
|
|
|
|
+ this.plugin.managers.structure.measurement.setOptions(options);
|
|
|
|
+ }
|
|
|
|
|
|
- const selections = obj.data.source as PluginStateObject.Molecule.Structure.Selections
|
|
|
|
|
|
+ render() {
|
|
|
|
+ const measurements = this.plugin.managers.structure.measurement.state;
|
|
|
|
|
|
- return <div className='msp-btn-row-group' key={obj.id} onMouseEnter={this.highlight(obj.data.repr, selections)} onMouseLeave={this.clearHighlight()}>
|
|
|
|
- <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus(selections)}>
|
|
|
|
- <span dangerouslySetInnerHTML={{ __html: this.getLabel(selections) }} />
|
|
|
|
- </button>
|
|
|
|
- <IconButton isSmall={true} customClass='msp-form-control' onClick={this.delete(cell)} icon='remove' style={{ width: '52px' }} title='Delete' />
|
|
|
|
- <IconButton isSmall={true} customClass='msp-form-control' onClick={this.toggleVisibility(cell)} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={cell.state.isHidden} />
|
|
|
|
- </div>
|
|
|
|
|
|
+ return <ExpandGroup header='Options'>
|
|
|
|
+ <ParameterControls params={StructureMeasurementParams} values={measurements.options} onChangeObject={this.changed} isDisabled={this.state.isDisabled} />
|
|
|
|
+ </ExpandGroup>;
|
|
}
|
|
}
|
|
|
|
+}
|
|
|
|
|
|
- getData() {
|
|
|
|
|
|
+class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasurementCell }> {
|
|
|
|
+ componentDidMount() {
|
|
|
|
+ this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
|
|
|
|
+ if (State.ObjectEvent.isCell(e, this.props.cell)) {
|
|
|
|
+ this.forceUpdate();
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
|
|
|
|
+ get selections() {
|
|
|
|
+ return this.props.cell.obj?.data.source as PluginStateObject.Molecule.Structure.Selections | undefined;
|
|
}
|
|
}
|
|
|
|
|
|
- renderControls() {
|
|
|
|
- const labels: JSX.Element[] = [];
|
|
|
|
- const distances: JSX.Element[] = [];
|
|
|
|
- const angles: JSX.Element[] = [];
|
|
|
|
- const dihedrals: JSX.Element[] = [];
|
|
|
|
|
|
+ delete = () => {
|
|
|
|
+ PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent, removeParentGhosts: true });
|
|
|
|
+ };
|
|
|
|
|
|
- const measurements = this.plugin.helpers.measurement.getMeasurements();
|
|
|
|
|
|
+ toggleVisibility = (e: React.MouseEvent<HTMLElement>) => {
|
|
|
|
+ e.preventDefault();
|
|
|
|
+ PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent });
|
|
|
|
+ e.currentTarget.blur();
|
|
|
|
+ }
|
|
|
|
|
|
- for (const d of measurements.labels) {
|
|
|
|
- const row = this.getRow(d)
|
|
|
|
- if (row) labels.push(row)
|
|
|
|
- }
|
|
|
|
|
|
+ highlight = () => {
|
|
|
|
+ const selections = this.selections;
|
|
|
|
+ if (!selections) return;
|
|
|
|
|
|
- for (const d of measurements.distances) {
|
|
|
|
- const row = this.getRow(d)
|
|
|
|
- if (row) distances.push(row)
|
|
|
|
|
|
+ this.plugin.interactivity.lociHighlights.clearHighlights();
|
|
|
|
+ for (const d of selections.data) {
|
|
|
|
+ this.plugin.interactivity.lociHighlights.highlight({ loci: d.loci }, false);
|
|
}
|
|
}
|
|
|
|
+ this.plugin.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false);
|
|
|
|
+ }
|
|
|
|
|
|
- for (const d of measurements.angles) {
|
|
|
|
- const row = this.getRow(d)
|
|
|
|
- if (row) angles.push(row)
|
|
|
|
|
|
+ clearHighlight = () => {
|
|
|
|
+ this.plugin.interactivity.lociHighlights.clearHighlights();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ focus = () => {
|
|
|
|
+ const selections = this.selections;
|
|
|
|
+ if (!selections) return;
|
|
|
|
+
|
|
|
|
+ const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data))
|
|
|
|
+ if (sphere) {
|
|
|
|
+ const { extraRadius, minRadius, durationMs } = MeasurementFocusOptions;
|
|
|
|
+ const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
|
|
|
+ this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
|
|
}
|
|
}
|
|
|
|
+ }
|
|
|
|
|
|
- for (const d of measurements.dihedrals) {
|
|
|
|
- const row = this.getRow(d)
|
|
|
|
- if (row) dihedrals.push(row)
|
|
|
|
|
|
+ get label() {
|
|
|
|
+ const selections = this.selections;
|
|
|
|
+ switch (selections?.data.length) {
|
|
|
|
+ case 1: return lociLabel(selections.data[0].loci, { condensed: true })
|
|
|
|
+ case 2: return distanceLabel(toLociBundle(selections.data), { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel })
|
|
|
|
+ case 3: return angleLabel(toLociBundle(selections.data), { condensed: true })
|
|
|
|
+ case 4: return dihedralLabel(toLociBundle(selections.data), { condensed: true })
|
|
|
|
+ default: return ''
|
|
}
|
|
}
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ render() {
|
|
|
|
+ const { cell } = this.props;
|
|
|
|
+ const { obj } = cell;
|
|
|
|
+ if (!obj) return null;
|
|
|
|
|
|
- return <div>
|
|
|
|
- {labels.length > 0 && <div>
|
|
|
|
- <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Labels</span></div>
|
|
|
|
- <div className='msp-control-offset'>{labels}</div>
|
|
|
|
- </div>}
|
|
|
|
- {distances.length > 0 && <div>
|
|
|
|
- <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Distances</span></div>
|
|
|
|
- <div className='msp-control-offset'>{distances}</div>
|
|
|
|
- </div>}
|
|
|
|
- {angles.length > 0 && <div>
|
|
|
|
- <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Angles</span></div>
|
|
|
|
- <div className='msp-control-offset'>{angles}</div>
|
|
|
|
- </div>}
|
|
|
|
- {dihedrals.length > 0 && <div>
|
|
|
|
- <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Dihedrals</span></div>
|
|
|
|
- <div className='msp-control-offset'>{dihedrals}</div>
|
|
|
|
- </div>}
|
|
|
|
|
|
+ return <div className='msp-btn-row-group' key={obj.id} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
|
|
|
|
+ <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus}>
|
|
|
|
+ <span dangerouslySetInnerHTML={{ __html: this.label }} />
|
|
|
|
+ </button>
|
|
|
|
+ <IconButton isSmall={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ width: '52px' }} title='Delete' />
|
|
|
|
+ <IconButton isSmall={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
+
|
|
}
|
|
}
|
|
|
|
|
|
function toLociBundle(data: FiniteArray<{ loci: Loci }, any>): { loci: FiniteArray<Loci, any> } {
|
|
function toLociBundle(data: FiniteArray<{ loci: Loci }, any>): { loci: FiniteArray<Loci, any> } {
|