volume.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import Add from '@material-ui/icons/Add';
  8. import BlurOn from '@material-ui/icons/BlurOn';
  9. import Check from '@material-ui/icons/Check';
  10. import ErrorSvg from '@material-ui/icons/Error';
  11. import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
  12. import MoreHoriz from '@material-ui/icons/MoreHoriz';
  13. import VisibilityOffOutlined from '@material-ui/icons/VisibilityOffOutlined';
  14. import VisibilityOutlined from '@material-ui/icons/VisibilityOutlined';
  15. import * as React from 'react';
  16. import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
  17. import { VolumeHierarchyManager } from '../../mol-plugin-state/manager/volume/hierarchy';
  18. import { VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state';
  19. import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
  20. import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
  21. import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
  22. import { State, StateSelection, StateTransform } from '../../mol-state';
  23. import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
  24. import { ActionMenu } from '../controls/action-menu';
  25. import { Button, ExpandGroup, IconButton } from '../controls/common';
  26. import { ApplyActionControl } from '../state/apply-action';
  27. import { UpdateTransformControl } from '../state/update-transform';
  28. import { BindingsHelp } from '../viewport/help';
  29. import { PluginCommands } from '../../mol-plugin/commands';
  30. interface VolumeStreamingControlState extends CollapsableState {
  31. isBusy: boolean
  32. }
  33. export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStreamingControlState> {
  34. protected defaultState(): VolumeStreamingControlState {
  35. return {
  36. header: 'Volume Streaming',
  37. isCollapsed: false,
  38. isBusy: false,
  39. isHidden: true,
  40. brand: { accent: 'cyan', svg: BlurOn }
  41. };
  42. }
  43. componentDidMount() {
  44. // TODO: do not hide this but instead show some help text??
  45. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => {
  46. this.setState({
  47. isHidden: !this.canEnable(),
  48. description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
  49. });
  50. });
  51. this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
  52. if (StateTransform.hasTag(e.cell.transform, VolumeStreaming.RootTag)) this.forceUpdate();
  53. });
  54. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  55. this.setState({ isBusy: v });
  56. });
  57. }
  58. get pivot() {
  59. return this.plugin.managers.structure.hierarchy.selection.structures[0];
  60. }
  61. canEnable() {
  62. const { selection } = this.plugin.managers.structure.hierarchy;
  63. if (selection.structures.length !== 1) return false;
  64. const pivot = this.pivot.cell;
  65. if (!pivot.obj) return false;
  66. return !!InitVolumeStreaming.definition.isApplicable?.(pivot.obj, pivot.transform, this.plugin);
  67. }
  68. renderEnable() {
  69. const pivot = this.pivot;
  70. if (!pivot.cell.parent) return null;
  71. const root = StateSelection.findTagInSubtree(pivot.cell.parent.tree, this.pivot.cell.transform.ref, VolumeStreaming.RootTag);
  72. const rootCell = root && pivot.cell.parent.cells.get(root);
  73. const simpleApply = rootCell && rootCell.status === 'error'
  74. ? { header: 'Error enabling', icon: ErrorSvg, title: rootCell.errorText }
  75. : rootCell && rootCell.obj?.data.entries.length === 0
  76. ? { header: 'Error enabling', icon: ErrorSvg, title: 'No entry for streaming found' }
  77. : { header: 'Enable', icon: Check, title: 'Enable' };
  78. return <ApplyActionControl state={pivot.cell.parent} action={InitVolumeStreaming} initiallyCollapsed={true} nodeRef={pivot.cell.transform.ref} simpleApply={simpleApply} />;
  79. }
  80. renderParams() {
  81. const pivot = this.pivot;
  82. if (!pivot.cell.parent) return null;
  83. const bindings = pivot.volumeStreaming?.cell.transform.params?.entry.params.view.name === 'selection-box' && this.plugin.state.behaviors.cells.get(FocusLoci.id)?.params?.values?.bindings;
  84. return <>
  85. <UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin autoHideApply />
  86. {bindings && <ExpandGroup header='Controls Help'>
  87. <BindingsHelp bindings={bindings} />
  88. </ExpandGroup>}
  89. </>;
  90. }
  91. renderControls() {
  92. const pivot = this.pivot;
  93. if (!pivot) return null;
  94. if (!pivot.volumeStreaming) return this.renderEnable();
  95. return this.renderParams();
  96. }
  97. }
  98. interface VolumeSourceControlState extends CollapsableState {
  99. isBusy: boolean,
  100. show?: 'hierarchy' | 'add-repr'
  101. }
  102. export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceControlState> {
  103. protected defaultState(): VolumeSourceControlState {
  104. return {
  105. header: 'Volume',
  106. isCollapsed: false,
  107. isBusy: false,
  108. isHidden: true,
  109. brand: { accent: 'purple', svg: BlurOn }
  110. };
  111. }
  112. componentDidMount() {
  113. this.subscribe(this.plugin.managers.volume.hierarchy.behaviors.selection, sel => {
  114. this.setState({ isHidden: sel.hierarchy.volumes.length === 0 });
  115. });
  116. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  117. this.setState({ isBusy: v });
  118. });
  119. }
  120. private item = (ref: VolumeRef) => {
  121. const selected = this.plugin.managers.volume.hierarchy.selection;
  122. const label = ref.cell.obj?.label || 'Volume';
  123. const item: ActionMenu.Item = { kind: 'item', label: label || ref.kind, selected: selected === ref, value: ref };
  124. return item;
  125. }
  126. get hierarchyItems() {
  127. const mng = this.plugin.managers.volume.hierarchy;
  128. const { current } = mng;
  129. const ret: ActionMenu.Items = [];
  130. for (let ref of current.volumes) {
  131. ret.push(this.item(ref));
  132. }
  133. return ret;
  134. }
  135. get addActions(): ActionMenu.Items {
  136. const mng = this.plugin.managers.volume.hierarchy;
  137. const current = mng.selection;
  138. const ret: ActionMenu.Items = [
  139. ...VolumeHierarchyManager.getRepresentationTypes(this.plugin, current)
  140. .map(t => ActionMenu.Item(t[1], () => mng.addRepresentation(current!, t[0])))
  141. ];
  142. return ret;
  143. }
  144. get isEmpty() {
  145. const { volumes } = this.plugin.managers.volume.hierarchy.current;
  146. return volumes.length === 0;
  147. }
  148. get label() {
  149. const selected = this.plugin.managers.volume.hierarchy.selection;
  150. if (!selected) return 'Nothing Selected';
  151. return selected?.cell.obj?.label || 'Volume';
  152. }
  153. selectCurrent: ActionMenu.OnSelect = (item) => {
  154. this.toggleHierarchy();
  155. if (!item) return;
  156. this.plugin.managers.volume.hierarchy.setCurrent(item.value as VolumeRef);
  157. }
  158. selectAdd: ActionMenu.OnSelect = (item) => {
  159. if (!item) return;
  160. this.setState({ show: void 0 });
  161. (item.value as any)();
  162. }
  163. toggleHierarchy = () => this.setState({ show: this.state.show !== 'hierarchy' ? 'hierarchy' : void 0 });
  164. toggleAddRepr = () => this.setState({ show: this.state.show !== 'add-repr' ? 'add-repr' : void 0 });
  165. renderControls() {
  166. const disabled = this.state.isBusy || this.isEmpty;
  167. const label = this.label;
  168. const selected = this.plugin.managers.volume.hierarchy.selection;
  169. return <>
  170. <div className='msp-flex-row' style={{ marginTop: '1px' }}>
  171. <Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button>
  172. {!this.isEmpty && <IconButton svg={Add} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />}
  173. </div>
  174. {this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectCurrent} />}
  175. {this.state.show === 'add-repr' && <ActionMenu items={this.addActions} onSelect={this.selectAdd} />}
  176. {selected && selected.representations.length > 0 && <div style={{ marginTop: '6px' }}>
  177. {selected.representations.map(r => <VolumeRepresentationControls key={r.cell.transform.ref} representation={r} />)}
  178. </div>}
  179. </>;
  180. }
  181. }
  182. type VolumeRepresentationEntryActions = 'update'
  183. class VolumeRepresentationControls extends PurePluginUIComponent<{ representation: VolumeRepresentationRef }, { action?: VolumeRepresentationEntryActions }> {
  184. state = { action: void 0 as VolumeRepresentationEntryActions | undefined }
  185. componentDidMount() {
  186. this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
  187. if (State.ObjectEvent.isCell(e, this.props.representation.cell)) this.forceUpdate();
  188. });
  189. }
  190. remove = () => this.plugin.managers.volume.hierarchy.remove([ this.props.representation ], true);
  191. toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
  192. e.preventDefault();
  193. e.currentTarget.blur();
  194. this.plugin.managers.volume.hierarchy.toggleVisibility([ this.props.representation ]);
  195. }
  196. toggleUpdate = () => this.setState({ action: this.state.action === 'update' ? void 0 : 'update' });
  197. highlight = (e: React.MouseEvent<HTMLElement>) => {
  198. e.preventDefault();
  199. if (!this.props.representation.cell.parent) return;
  200. PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.representation.cell.parent!, ref: this.props.representation.cell.transform.ref });
  201. }
  202. clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
  203. e.preventDefault();
  204. PluginCommands.Interactivity.ClearHighlights(this.plugin);
  205. }
  206. focus = () => {
  207. const repr = this.props.representation;
  208. const objects = this.props.representation.cell.obj?.data.repr.renderObjects;
  209. if (repr.cell.state.isHidden) this.plugin.managers.volume.hierarchy.toggleVisibility([this.props.representation], 'show');
  210. this.plugin.managers.camera.focusRenderObjects(objects, { extraRadius: 1 });
  211. }
  212. render() {
  213. const repr = this.props.representation.cell;
  214. return <>
  215. <div className='msp-flex-row'>
  216. <Button noOverflow className='msp-control-button-label' title={`${repr.obj?.label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
  217. {repr.obj?.label}
  218. <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{repr.obj?.description}</small>
  219. </Button>
  220. <IconButton svg={repr.state.isHidden ? VisibilityOffOutlined : VisibilityOutlined} toggleState={false} onClick={this.toggleVisible} title={`${repr.state.isHidden ? 'Show' : 'Hide'} component`} small className='msp-form-control' flex />
  221. <IconButton svg={DeleteOutlined} onClick={this.remove} title='Remove' small />
  222. <IconButton svg={MoreHoriz} onClick={this.toggleUpdate} title='Actions' toggleState={this.state.action === 'update'} />
  223. </div>
  224. {this.state.action === 'update' && !!repr.parent && <div style={{ marginBottom: '6px' }} className='msp-accent-offset'>
  225. <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin />
  226. </div>}
  227. </>;
  228. }
  229. }