selection.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import * as React from 'react';
  7. import { CollapsableControls, CollapsableState } from '../base';
  8. import { StructureSelectionQueries, SelectionModifier } from '../../util/structure-selection-helper';
  9. import { ButtonSelect, Options } from '../controls/common';
  10. import { PluginCommands } from '../../command';
  11. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  12. import { Interactivity } from '../../util/interactivity';
  13. import { ParameterControls } from '../controls/parameters';
  14. import { stripTags } from '../../../mol-util/string';
  15. import { StructureElement } from '../../../mol-model/structure';
  16. const SSQ = StructureSelectionQueries
  17. const DefaultQueries: (keyof typeof SSQ)[] = [
  18. 'all', 'polymer', 'trace', 'protein', 'nucleic', 'water', 'branched', 'ligand', 'nonStandardPolymer',
  19. 'surroundings', 'complement', 'bonded'
  20. ]
  21. const StructureSelectionParams = {
  22. granularity: Interactivity.Params.granularity,
  23. }
  24. interface StructureSelectionControlsState extends CollapsableState {
  25. minRadius: number,
  26. extraRadius: number,
  27. durationMs: number,
  28. isDisabled: boolean
  29. }
  30. export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
  31. componentDidMount() {
  32. this.subscribe(this.plugin.events.interactivity.selectionUpdated, () => {
  33. this.forceUpdate()
  34. });
  35. this.subscribe(this.plugin.events.interactivity.propsUpdated, () => {
  36. this.forceUpdate()
  37. });
  38. this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v }))
  39. }
  40. get stats() {
  41. const stats = this.plugin.helpers.structureSelectionManager.stats
  42. if (stats.structureCount === 0 || stats.elementCount === 0) {
  43. return 'Selected nothing'
  44. } else {
  45. return `Selected ${stripTags(stats.label)}`
  46. }
  47. }
  48. focus = () => {
  49. const { extraRadius, minRadius, durationMs } = this.state
  50. if (this.plugin.helpers.structureSelectionManager.stats.elementCount === 0) return
  51. const principalAxes = this.plugin.helpers.structureSelectionManager.getPrincipalAxes();
  52. const { origin, dirA, dirC } = principalAxes.boxAxes
  53. const { sphere } = this.plugin.helpers.structureSelectionManager.getBoundary()
  54. const radius = Math.max(sphere.radius + extraRadius, minRadius);
  55. this.plugin.canvas3d?.camera.focus(origin, radius, durationMs, dirA, dirC);
  56. }
  57. focusLoci(loci: StructureElement.Loci) {
  58. return () => {
  59. const { extraRadius, minRadius, durationMs } = this.state
  60. if (this.plugin.helpers.structureSelectionManager.stats.elementCount === 0) return
  61. const { sphere } = StructureElement.Loci.getBoundary(loci)
  62. const radius = Math.max(sphere.radius + extraRadius, minRadius);
  63. this.plugin.canvas3d?.camera.focus(sphere.center, radius, durationMs);
  64. }
  65. }
  66. measureDistance = () => {
  67. const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
  68. this.plugin.helpers.measurement.addDistance(loci[0].loci, loci[1].loci);
  69. }
  70. measureAngle = () => {
  71. const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
  72. this.plugin.helpers.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci);
  73. }
  74. setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
  75. if (p.name === 'granularity') {
  76. PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { granularity: p.value } });
  77. }
  78. }
  79. get values () {
  80. return {
  81. granularity: this.plugin.interactivity.props.granularity,
  82. }
  83. }
  84. set = (modifier: SelectionModifier, value: string) => {
  85. const query = SSQ[value as keyof typeof SSQ]
  86. this.plugin.helpers.structureSelection.set(modifier, query.query, false)
  87. }
  88. add = (value: string) => this.set('add', value)
  89. remove = (value: string) => this.set('remove', value)
  90. only = (value: string) => this.set('only', value)
  91. queries = Options(Object.keys(StructureSelectionQueries)
  92. .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string])
  93. .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ)));
  94. controls = <div className='msp-control-row'>
  95. <div className='msp-select-row'>
  96. <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}>
  97. <optgroup label='Select'>
  98. {this.queries}
  99. </optgroup>
  100. </ButtonSelect>
  101. <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}>
  102. <optgroup label='Deselect'>
  103. {this.queries}
  104. </optgroup>
  105. </ButtonSelect>
  106. <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}>
  107. <optgroup label='Only'>
  108. {this.queries}
  109. </optgroup>
  110. </ButtonSelect>
  111. </div>
  112. </div>
  113. defaultState() {
  114. return {
  115. isCollapsed: false,
  116. header: 'Selection',
  117. minRadius: 8,
  118. extraRadius: 4,
  119. durationMs: 250,
  120. isDisabled: false
  121. } as S
  122. }
  123. renderControls() {
  124. const latest: JSX.Element[] = [];
  125. const mng = this.plugin.helpers.structureSelectionManager;
  126. // TODO: fix the styles, move them to CSS
  127. for (let i = 0, _i = Math.min(3, mng.latestLoci.length); i < _i; i++) {
  128. const e = mng.latestLoci[i];
  129. latest.push(<li key={e!.label}>
  130. <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
  131. title='Click to focus.' onClick={this.focusLoci(e.loci)}>
  132. <span dangerouslySetInnerHTML={{ __html: e.label }} />
  133. </button>
  134. {/* <div>
  135. <IconButton icon='remove' title='Remove' onClick={() => {}} />
  136. </div> */}
  137. </li>)
  138. }
  139. return <div>
  140. <div className='msp-control-row msp-row-text'>
  141. <button className='msp-btn msp-btn-block' onClick={this.focus}>
  142. <span className={`msp-icon msp-icon-focus-on-visual`} style={{ position: 'absolute', left: '5px' }} />
  143. {this.stats}
  144. </button>
  145. </div>
  146. <ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
  147. {this.controls}
  148. { latest.length > 0 &&
  149. <>
  150. <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Latest Selections &amp; Measurement</span></div>
  151. <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
  152. {latest}
  153. </ul>
  154. {latest.length >= 2 &&
  155. <div className='msp-control-row msp-row-text'>
  156. <button className='msp-btn msp-btn-block' onClick={this.measureDistance} title='Measure distance between latest 2 selections'>
  157. Measure Distance
  158. </button>
  159. </div>}
  160. {latest.length >= 3 &&
  161. <div className='msp-control-row msp-row-text'>
  162. <button className='msp-btn msp-btn-block' onClick={this.measureAngle} title='Measure angle between latest 3 selections'>
  163. Measure Angle
  164. </button>
  165. </div>}
  166. </>}
  167. </div>
  168. }
  169. }