volume.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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 * as React from 'react';
  8. import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
  9. import { VolumeHierarchyManager } from '../../mol-plugin-state/manager/volume/hierarchy';
  10. import { LazyVolumeRef, VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state';
  11. import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
  12. import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
  13. import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
  14. import { State, StateObjectCell, StateObjectSelector, StateSelection, StateTransform } from '../../mol-state';
  15. import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
  16. import { ActionMenu } from '../controls/action-menu';
  17. import { Button, ExpandGroup, IconButton } from '../controls/common';
  18. import { ApplyActionControl } from '../state/apply-action';
  19. import { UpdateTransformControl } from '../state/update-transform';
  20. import { BindingsHelp } from '../viewport/help';
  21. import { PluginCommands } from '../../mol-plugin/commands';
  22. import { BlurOnSvg, ErrorSvg, CheckSvg, AddSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg } from '../controls/icons';
  23. import { PluginStateObject } from '../../mol-plugin-state/objects';
  24. import { StateTransforms } from '../../mol-plugin-state/transforms';
  25. import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
  26. interface VolumeStreamingControlState extends CollapsableState {
  27. isBusy: boolean
  28. }
  29. export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStreamingControlState> {
  30. protected defaultState(): VolumeStreamingControlState {
  31. return {
  32. header: 'Volume Streaming',
  33. isCollapsed: false,
  34. isBusy: false,
  35. isHidden: true,
  36. brand: { accent: 'cyan', svg: BlurOnSvg }
  37. };
  38. }
  39. componentDidMount() {
  40. // TODO: do not hide this but instead show some help text??
  41. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => {
  42. this.setState({
  43. isHidden: !this.canEnable(),
  44. description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
  45. });
  46. });
  47. this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
  48. if (StateTransform.hasTag(e.cell.transform, VolumeStreaming.RootTag)) this.forceUpdate();
  49. });
  50. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  51. this.setState({ isBusy: v });
  52. });
  53. }
  54. get pivot() {
  55. return this.plugin.managers.structure.hierarchy.selection.structures[0];
  56. }
  57. canEnable() {
  58. const { selection } = this.plugin.managers.structure.hierarchy;
  59. if (selection.structures.length !== 1) return false;
  60. const pivot = this.pivot.cell;
  61. if (!pivot.obj) return false;
  62. return !!InitVolumeStreaming.definition.isApplicable?.(pivot.obj, pivot.transform, this.plugin);
  63. }
  64. renderEnable() {
  65. const pivot = this.pivot;
  66. if (!pivot.cell.parent) return null;
  67. const root = StateSelection.findTagInSubtree(pivot.cell.parent.tree, this.pivot.cell.transform.ref, VolumeStreaming.RootTag);
  68. const rootCell = root && pivot.cell.parent.cells.get(root);
  69. const simpleApply = rootCell && rootCell.status === 'error'
  70. ? { header: 'Error enabling', icon: ErrorSvg, title: rootCell.errorText }
  71. : rootCell && rootCell.obj?.data.entries.length === 0
  72. ? { header: 'Error enabling', icon: ErrorSvg, title: 'No entry for streaming found' }
  73. : { header: 'Enable', icon: CheckSvg, title: 'Enable' };
  74. return <ApplyActionControl state={pivot.cell.parent} action={InitVolumeStreaming} initiallyCollapsed={true} nodeRef={pivot.cell.transform.ref} simpleApply={simpleApply} />;
  75. }
  76. renderParams() {
  77. const pivot = this.pivot;
  78. if (!pivot.cell.parent) return null;
  79. const bindings = pivot.volumeStreaming?.cell.transform.params?.entry.params.view.name === 'selection-box' && this.plugin.state.behaviors.cells.get(FocusLoci.id)?.params?.values?.bindings;
  80. return <>
  81. <UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin />
  82. {bindings && <ExpandGroup header='Controls Help'>
  83. <BindingsHelp bindings={bindings} />
  84. </ExpandGroup>}
  85. </>;
  86. }
  87. renderControls() {
  88. const pivot = this.pivot;
  89. if (!pivot) return null;
  90. if (!pivot.volumeStreaming) return this.renderEnable();
  91. return this.renderParams();
  92. }
  93. }
  94. interface VolumeSourceControlState extends CollapsableState {
  95. isBusy: boolean,
  96. loadingLabel?: string,
  97. show?: 'hierarchy' | 'add-repr'
  98. }
  99. export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceControlState> {
  100. protected defaultState(): VolumeSourceControlState {
  101. return {
  102. header: 'Volume',
  103. isCollapsed: false,
  104. isBusy: false,
  105. isHidden: true,
  106. brand: { accent: 'purple', svg: BlurOnSvg }
  107. };
  108. }
  109. componentDidMount() {
  110. this.subscribe(this.plugin.managers.volume.hierarchy.behaviors.selection, sel => {
  111. this.setState({ isHidden: sel.hierarchy.volumes.length === 0 && sel.hierarchy.lazyVolumes.length === 0 });
  112. });
  113. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  114. this.setState({ isBusy: v });
  115. });
  116. }
  117. private item = (ref: VolumeRef | LazyVolumeRef) => {
  118. const selected = this.plugin.managers.volume.hierarchy.selection;
  119. const label = ref.cell.obj?.label || 'Volume';
  120. const item: ActionMenu.Item = {
  121. kind: 'item',
  122. label: (ref.kind === 'lazy-volume' ? 'Load ' : '') + (label || ref.kind),
  123. selected: selected === ref,
  124. value: ref
  125. };
  126. return item;
  127. }
  128. get hierarchyItems() {
  129. const mng = this.plugin.managers.volume.hierarchy;
  130. const { current } = mng;
  131. const ret: ActionMenu.Items = [];
  132. for (const ref of current.volumes) {
  133. ret.push(this.item(ref));
  134. }
  135. for (const ref of current.lazyVolumes) {
  136. ret.push(this.item(ref));
  137. }
  138. return ret;
  139. }
  140. get addActions(): ActionMenu.Items {
  141. const mng = this.plugin.managers.volume.hierarchy;
  142. const current = mng.selection;
  143. const ret: ActionMenu.Items = [
  144. ...VolumeHierarchyManager.getRepresentationTypes(this.plugin, current)
  145. .map(t => ActionMenu.Item(t[1], () => mng.addRepresentation(current!, t[0])))
  146. ];
  147. return ret;
  148. }
  149. get isEmpty() {
  150. const { volumes, lazyVolumes } = this.plugin.managers.volume.hierarchy.current;
  151. return volumes.length === 0 && lazyVolumes.length === 0;
  152. }
  153. get label() {
  154. if (this.state.loadingLabel) return `Loading ${this.state.loadingLabel}...`;
  155. const selected = this.plugin.managers.volume.hierarchy.selection;
  156. if (!selected) return 'Nothing Selected';
  157. return selected?.cell.obj?.label || 'Volume';
  158. }
  159. selectCurrent: ActionMenu.OnSelect = (item) => {
  160. this.toggleHierarchy();
  161. if (!item) return;
  162. const current = item.value as VolumeRef | LazyVolumeRef;
  163. if (current.kind === 'volume') {
  164. this.plugin.managers.volume.hierarchy.setCurrent(current);
  165. } else {
  166. this.lazyLoad(current.cell);
  167. }
  168. }
  169. private async lazyLoad(cell: StateObjectCell<PluginStateObject.Volume.Lazy>) {
  170. const { url, isBinary, format, entryId, isovalues } = cell.obj!.data;
  171. this.setState({ isBusy: true, loadingLabel: cell.obj!.label });
  172. try {
  173. const plugin = this.plugin;
  174. await plugin.dataTransaction(async () => {
  175. const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
  176. const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
  177. const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
  178. if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
  179. const repr = plugin.build();
  180. for (const iso of isovalues) {
  181. repr
  182. .to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume)
  183. .apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
  184. type: 'isosurface',
  185. typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
  186. color: 'uniform',
  187. colorParams: { value: iso.color }
  188. }));
  189. }
  190. await repr.commit();
  191. await plugin.build().delete(cell).commit();
  192. });
  193. } finally {
  194. this.setState({ isBusy: false, loadingLabel: void 0 });
  195. }
  196. }
  197. selectAdd: ActionMenu.OnSelect = (item) => {
  198. if (!item) return;
  199. this.setState({ show: void 0 });
  200. (item.value as any)();
  201. }
  202. toggleHierarchy = () => this.setState({ show: this.state.show !== 'hierarchy' ? 'hierarchy' : void 0 });
  203. toggleAddRepr = () => this.setState({ show: this.state.show !== 'add-repr' ? 'add-repr' : void 0 });
  204. renderControls() {
  205. const disabled = this.state.isBusy || this.isEmpty;
  206. const label = this.label;
  207. const selected = this.plugin.managers.volume.hierarchy.selection;
  208. return <>
  209. <div className='msp-flex-row' style={{ marginTop: '1px' }}>
  210. <Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button>
  211. {!this.isEmpty && selected && <IconButton svg={AddSvg} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />}
  212. </div>
  213. {this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectCurrent} />}
  214. {this.state.show === 'add-repr' && <ActionMenu items={this.addActions} onSelect={this.selectAdd} />}
  215. {selected && selected.representations.length > 0 && <div style={{ marginTop: '6px' }}>
  216. {selected.representations.map(r => <VolumeRepresentationControls key={r.cell.transform.ref} representation={r} />)}
  217. </div>}
  218. </>;
  219. }
  220. }
  221. type VolumeRepresentationEntryActions = 'update'
  222. class VolumeRepresentationControls extends PurePluginUIComponent<{ representation: VolumeRepresentationRef }, { action?: VolumeRepresentationEntryActions }> {
  223. state = { action: void 0 as VolumeRepresentationEntryActions | undefined }
  224. componentDidMount() {
  225. this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
  226. if (State.ObjectEvent.isCell(e, this.props.representation.cell)) this.forceUpdate();
  227. });
  228. }
  229. remove = () => this.plugin.managers.volume.hierarchy.remove([this.props.representation], true);
  230. toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
  231. e.preventDefault();
  232. e.currentTarget.blur();
  233. this.plugin.managers.volume.hierarchy.toggleVisibility([this.props.representation]);
  234. }
  235. toggleUpdate = () => this.setState({ action: this.state.action === 'update' ? void 0 : 'update' });
  236. highlight = (e: React.MouseEvent<HTMLElement>) => {
  237. e.preventDefault();
  238. if (!this.props.representation.cell.parent) return;
  239. PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.representation.cell.parent!, ref: this.props.representation.cell.transform.ref });
  240. }
  241. clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
  242. e.preventDefault();
  243. PluginCommands.Interactivity.ClearHighlights(this.plugin);
  244. }
  245. focus = () => {
  246. const repr = this.props.representation;
  247. const objects = this.props.representation.cell.obj?.data.repr.renderObjects;
  248. if (repr.cell.state.isHidden) this.plugin.managers.volume.hierarchy.toggleVisibility([this.props.representation], 'show');
  249. this.plugin.managers.camera.focusRenderObjects(objects, { extraRadius: 1 });
  250. }
  251. render() {
  252. const repr = this.props.representation.cell;
  253. return <>
  254. <div className='msp-flex-row'>
  255. <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' }}>
  256. {repr.obj?.label}
  257. <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{repr.obj?.description}</small>
  258. </Button>
  259. <IconButton svg={repr.state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} onClick={this.toggleVisible} title={`${repr.state.isHidden ? 'Show' : 'Hide'} component`} small className='msp-form-control' flex />
  260. <IconButton svg={DeleteOutlinedSvg} onClick={this.remove} title='Remove' small />
  261. <IconButton svg={MoreHorizSvg} onClick={this.toggleUpdate} title='Actions' toggleState={this.state.action === 'update'} />
  262. </div>
  263. {this.state.action === 'update' && !!repr.parent && <div style={{ marginBottom: '6px' }} className='msp-accent-offset'>
  264. <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin />
  265. </div>}
  266. </>;
  267. }
  268. }