/** * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose */ import * as React from 'react'; import { Loci } from '../../mol-model/loci'; import { StructureElement } from '../../mol-model/structure'; import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement'; import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection'; import { PluginStateObject } from '../../mol-plugin-state/objects'; import { PluginCommands } from '../../mol-plugin/commands'; import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label'; import { FiniteArray } from '../../mol-util/type-helpers'; import { PurePluginUIComponent, CollapsableControls } from '../base'; import { ActionMenu } from '../controls/action-menu'; import { ExpandGroup, IconButton, ToggleButton, Button } from '../controls/common'; import { Icon } from '../controls/icons'; import { ParameterControls } from '../controls/parameters'; import { UpdateTransformControl } from '../state/update-transform'; // TODO details, options (e.g. change text for labels) export class StructureMeasurementsControls extends CollapsableControls { defaultState() { return { isCollapsed: false, header: 'Measurements', brand: { name: 'Msr', accent: 'gray' as const } } } renderControls() { return <> } } export class MeasurementList extends PurePluginUIComponent { componentDidMount() { this.subscribe(this.plugin.managers.structure.measurement.behaviors.state, () => { this.forceUpdate(); }); } renderGroup(cells: ReadonlyArray, header: string) { const group: JSX.Element[] = []; for (const cell of cells) { if (cell.obj) group.push() } return group.length ? {group} : null; } render() { 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')} {this.renderGroup(measurements.orientations, 'Orientations')}
; } } export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boolean, action?: 'add' | 'options' }> { state = { isBusy: false, action: void 0 as 'add' | 'options' | undefined } componentDidMount() { this.subscribe(this.selection.events.additionsHistoryUpdated, () => { this.forceUpdate(); }); this.subscribe(this.plugin.behaviors.state.isBusy, v => { this.setState({ isBusy: v }); }); } get selection() { return this.plugin.managers.structure.selection; } measureDistance = () => { const loci = this.plugin.managers.structure.selection.additionsHistory; this.plugin.managers.structure.measurement.addDistance(loci[0].loci, loci[1].loci); } measureAngle = () => { const loci = this.plugin.managers.structure.selection.additionsHistory; this.plugin.managers.structure.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci); } measureDihedral = () => { const loci = this.plugin.managers.structure.selection.additionsHistory; this.plugin.managers.structure.measurement.addDihedral(loci[0].loci, loci[1].loci, loci[2].loci, loci[3].loci); } addLabel = () => { const loci = this.plugin.managers.structure.selection.additionsHistory; this.plugin.managers.structure.measurement.addLabel(loci[0].loci); } addOrientation = () => { // TODO: this should be possible to add for the whole selection const loci = this.plugin.managers.structure.selection.additionsHistory; this.plugin.managers.structure.measurement.addOrientation(loci[0].loci); } get actions(): ActionMenu.Items { const history = this.selection.additionsHistory; const ret: ActionMenu.Item[] = [ { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addLabel, disabled: history.length === 0 }, { kind: 'item', label: `Orientation ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addOrientation, disabled: history.length === 0 }, { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selections required)' : ' (top 2 selections)'}`, value: this.measureDistance, disabled: history.length < 2 }, { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selections required)' : ' (top 3 selections)'}`, value: this.measureAngle, disabled: history.length < 3 }, { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selections required)' : ' (top 4 selections)'}`, value: this.measureDihedral, disabled: history.length < 4 }, ]; return ret; } selectAction: ActionMenu.OnSelect = item => { this.toggleAdd(); if (!item) return; (item?.value as any)(); } toggleAdd = () => this.setState({ action: this.state.action === 'add' ? void 0 : 'add' }); toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options' }); highlight(loci: StructureElement.Loci) { this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci }, false); } moveHistory(e: StructureSelectionHistoryEntry, direction: 'up' | 'down') { this.plugin.managers.structure.selection.modifyHistory(e, direction, 4); } focusLoci(loci: StructureElement.Loci) { this.plugin.managers.camera.focusLoci(loci); } historyEntry(e: StructureSelectionHistoryEntry, idx: number) { const history = this.plugin.managers.structure.selection.additionsHistory; return
{history.length > 1 && this.moveHistory(e, 'up')} icon='up-thin' flex='20px' title={'Move up'} />} {history.length > 1 && this.moveHistory(e, 'down')} icon='down-thin' flex='20px' title={'Move down'} />} this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} icon='remove' flex title={'Remove'} />
; } add() { const history = this.plugin.managers.structure.selection.additionsHistory; const entries: JSX.Element[] = []; for (let i = 0, _i = Math.min(history.length, 4); i < _i; i++) { entries.push(this.historyEntry(history[i], i + 1)); } return <> {entries.length > 0 &&
{entries}
} {entries.length === 0 &&
Add one or more selections
} } render() { return <>
{this.state.action === 'add' && this.add()} {this.state.action === 'options' && } } } class MeasurementsOptions extends PurePluginUIComponent<{}, { isDisabled: boolean }> { state = { isDisabled: false } componentDidMount() { this.subscribe(this.plugin.managers.structure.measurement.behaviors.state, () => { this.forceUpdate(); }); this.subscribe(this.plugin.behaviors.state.isBusy, v => { console.log('isBusy', 'measurement opt', v) this.setState({ isDisabled: v }) }); } changed = (options: StructureMeasurementOptions) => { this.plugin.managers.structure.measurement.setOptions(options); } render() { const measurements = this.plugin.managers.structure.measurement.state; return
; } } class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasurementCell }, { showUpdate: boolean }> { state = { showUpdate: false } componentDidMount() { this.subscribe(this.plugin.events.state.cell.stateUpdated, e => { this.forceUpdate(); }); } get selections() { return this.props.cell.obj?.data.source as PluginStateObject.Molecule.Structure.Selections | undefined; } delete = () => { PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent, removeParentGhosts: true }); }; toggleVisibility = (e: React.MouseEvent) => { e.preventDefault(); PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent }); e.currentTarget.blur(); } highlight = () => { const selections = this.selections; if (!selections) return; this.plugin.managers.interactivity.lociHighlights.clearHighlights(); for (const d of selections.data) { this.plugin.managers.interactivity.lociHighlights.highlight({ loci: d.loci }, false); } this.plugin.managers.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false); } clearHighlight = () => { this.plugin.managers.interactivity.lociHighlights.clearHighlights(); } toggleUpdate = () => this.setState({ showUpdate: !this.state.showUpdate }); focus = () => { const selections = this.selections; if (!selections) return; const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data)) if (sphere) { this.plugin.managers.camera.focusSphere(sphere); } } 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 '' } } get actions(): ActionMenu.Items { this.props.cell.sourceRef return [ActionMenu.Item('Select This', 'flash', () => this.plugin.managers.structure.selection.fromSelections(this.props.cell.sourceRef!))]; } selectAction: ActionMenu.OnSelect = item => { if (!item) return; this.setState({ showUpdate: false }); (item?.value as any)(); } render() { const { cell } = this.props; const { obj } = cell; if (!obj) return null; return <>
{this.state.showUpdate && <>
} } } function toLociBundle(data: FiniteArray<{ loci: Loci }, any>): { loci: FiniteArray } { return { loci: (data.map(d => d.loci) as unknown as FiniteArray) } }