component.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { VisualQualityOptions } from '../../../mol-geo/geometry/base';
  7. import { InteractionsProvider } from '../../../mol-model-props/computed/interactions';
  8. import { Structure, StructureElement } from '../../../mol-model/structure';
  9. import { structureAreIntersecting, structureSubtract, structureUnion } from '../../../mol-model/structure/query/utils/structure-set';
  10. import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
  11. import { PluginContext } from '../../../mol-plugin/context';
  12. import { StateBuilder, StateTransformer } from '../../../mol-state';
  13. import { Task } from '../../../mol-task';
  14. import { UUID } from '../../../mol-util';
  15. import { arraySetAdd } from '../../../mol-util/array';
  16. import { ColorNames } from '../../../mol-util/color/names';
  17. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  18. import { StructureRepresentationProvider } from '../../builder/structure/provider';
  19. import { PluginComponent } from '../../component';
  20. import { StructureComponentParams } from '../../helpers/structure-component';
  21. import { setStructureOverpaint, clearStructureOverpaint } from '../../helpers/structure-overpaint';
  22. import { StructureSelectionQuery, StructureSelectionQueryOptions, StructureSelectionQueries } from '../../helpers/structure-selection-query';
  23. import { CustomStructureProperties } from '../../transforms/model';
  24. import { StructureRepresentation3D } from '../../transforms/representation';
  25. import { HierarchyRef, StructureComponentRef, StructureRef, StructureRepresentationRef } from './hierarchy-state';
  26. export { StructureComponentManager };
  27. interface StructureComponentManagerState {
  28. options: StructureComponentManager.Options
  29. }
  30. class StructureComponentManager extends PluginComponent<StructureComponentManagerState> {
  31. readonly events = {
  32. optionsUpdated: this.ev<undefined>()
  33. }
  34. get currentStructures() {
  35. return this.plugin.managers.structure.hierarchy.state.current.structures;
  36. }
  37. get pivotStructure(): StructureRef | undefined {
  38. return this.currentStructures[0];
  39. }
  40. async setOptions(options: StructureComponentManager.Options) {
  41. const interactionChanged = options.interactions !== this.state.options.interactions;
  42. this.updateState({ options });
  43. this.events.optionsUpdated.next();
  44. const update = this.dataState.build();
  45. for (const s of this.currentStructures) {
  46. for (const c of s.components) {
  47. this.updateReprParams(update, c);
  48. }
  49. if (s.currentFocus?.focus) this.updateReprParams(update, s.currentFocus.focus);
  50. if (s.currentFocus?.surroundings) this.updateReprParams(update, s.currentFocus.surroundings);
  51. }
  52. return this.plugin.dataTransaction(async () => {
  53. await this.plugin.runTask(this.dataState.updateTree(update));
  54. if (interactionChanged) await this.updateInterationProps();
  55. });
  56. }
  57. private updateReprParams(update: StateBuilder.Root, component: StructureComponentRef) {
  58. const { showHydrogens, visualQuality: quality } = this.state.options;
  59. const ignoreHydrogens = !showHydrogens;
  60. for (const r of component.representations) {
  61. if (r.cell.transform.transformer !== StructureRepresentation3D) continue;
  62. const params = r.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
  63. if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.quality !== quality) {
  64. update.to(r.cell).update(old => {
  65. old.type.params.ignoreHydrogens = ignoreHydrogens;
  66. old.type.params.quality = quality;
  67. });
  68. }
  69. }
  70. }
  71. private async updateInterationProps() {
  72. for (const s of this.currentStructures) {
  73. const interactionParams = InteractionsProvider.getParams(s.cell.obj?.data!);
  74. if (s.properties) {
  75. const params = s.properties.cell.transform.params;
  76. if (PD.areEqual(interactionParams, params, this.state.options.interactions)) continue;
  77. const b = this.dataState.build();
  78. b.to(s.properties.cell).update((old: StateTransformer.Params<CustomStructureProperties>) => {
  79. arraySetAdd(old.autoAttach, InteractionsProvider.descriptor.name);
  80. old.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
  81. });
  82. await this.plugin.runTask(this.dataState.updateTree(b));
  83. } else {
  84. const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
  85. const params = PD.getDefaultValues(pd);
  86. if (PD.areEqual(interactionParams, params.properties[InteractionsProvider.descriptor.name], this.state.options.interactions)) continue;
  87. arraySetAdd(params.autoAttach, InteractionsProvider.descriptor.name);
  88. params.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
  89. await this.plugin.builders.structure.insertStructureProperties(s.cell, params);
  90. }
  91. }
  92. }
  93. applyPreset<P = any, S = {}>(structures: ReadonlyArray<StructureRef>, provider: StructureRepresentationProvider<P, S>, params?: P): Promise<any> {
  94. return this.plugin.dataTransaction(async () => {
  95. await this.clearComponents(structures);
  96. for (const s of structures) {
  97. await this.plugin.builders.structure.representation.applyPreset(s.cell, provider, params);
  98. }
  99. });
  100. }
  101. clear(structures: ReadonlyArray<StructureRef>) {
  102. return this.clearComponents(structures);
  103. }
  104. selectThis(components: ReadonlyArray<StructureComponentRef>) {
  105. const mng = this.plugin.managers.structure.selection;
  106. mng.clear();
  107. for (const c of components) {
  108. const loci = Structure.toSubStructureElementLoci(c.structure.cell.obj!.data, c.cell.obj?.data!)
  109. mng.fromLoci('set', loci);
  110. }
  111. }
  112. modifyByCurrentSelection(components: ReadonlyArray<StructureComponentRef>, action: 'union' | 'subtract') {
  113. return this.plugin.runTask(Task.create('Subtract', async taskCtx => {
  114. const b = this.dataState.build();
  115. for (const c of components) {
  116. const selection = this.plugin.managers.structure.selection.getStructure(c.structure.cell.obj!.data);
  117. if (!selection || selection.elementCount === 0) continue;
  118. this.updateComponent(b, c, selection, action);
  119. }
  120. await this.dataState.updateTree(b).runInContext(taskCtx);
  121. }));
  122. }
  123. toggleVisibility(components: ReadonlyArray<StructureComponentRef>) {
  124. if (components.length === 0) return;
  125. const isHidden = !components[0].cell.state.isHidden;
  126. for (const c of components) {
  127. setSubtreeVisibility(this.dataState, c.cell.transform.ref, isHidden);
  128. }
  129. }
  130. removeRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot?: StructureRepresentationRef) {
  131. if (components.length === 0) return;
  132. const toRemove: HierarchyRef[] = [];
  133. if (pivot) {
  134. const index = components[0].representations.indexOf(pivot);
  135. if (index < 0) return;
  136. for (const c of components) {
  137. if (c.representations[index]) toRemove.push(c.representations[index]);
  138. }
  139. } else {
  140. for (const c of components) {
  141. for (const r of c.representations) {
  142. toRemove.push(r);
  143. }
  144. }
  145. }
  146. return this.plugin.managers.structure.hierarchy.remove(toRemove);
  147. }
  148. updateRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot: StructureRepresentationRef, params: StateTransformer.Params<StructureRepresentation3D>) {
  149. if (components.length === 0) return Promise.resolve();
  150. const index = components[0].representations.indexOf(pivot);
  151. if (index < 0) return Promise.resolve();
  152. const update = this.dataState.build();
  153. for (const c of components) {
  154. const repr = c.representations[index];
  155. if (!repr) continue;
  156. update.to(repr.cell).update(params);
  157. }
  158. return this.plugin.runTask(this.dataState.updateTree(update));
  159. }
  160. async addRepresentation(components: ReadonlyArray<StructureComponentRef>, type: string) {
  161. if (components.length === 0) return;
  162. const { showHydrogens, visualQuality: quality } = this.state.options;
  163. const ignoreHydrogens = !showHydrogens;
  164. const typeParams = { ignoreHydrogens, quality };
  165. for (const component of components) {
  166. await this.plugin.builders.structure.representation.addRepresentation(component.cell, {
  167. type: this.plugin.structureRepresentation.registry.get(type),
  168. typeParams
  169. });
  170. }
  171. }
  172. async add(params: StructureComponentManager.AddParams, structures?: ReadonlyArray<StructureRef>) {
  173. return this.plugin.dataTransaction(async () => {
  174. const xs = structures || this.currentStructures;
  175. if (xs.length === 0) return;
  176. const componentKey = UUID.create22();
  177. for (const s of xs) {
  178. const component = await this.plugin.builders.structure.tryCreateQueryComponent({
  179. structure: s.cell,
  180. query: params.selection,
  181. key: componentKey,
  182. label: params.label || (params.selection === StructureSelectionQueries.current ? 'Custom Selection' : ''),
  183. });
  184. if (params.representation === 'none' || !component) continue;
  185. await this.plugin.builders.structure.representation.addRepresentation(component, {
  186. type: this.plugin.structureRepresentation.registry.get(params.representation)
  187. });
  188. }
  189. });
  190. }
  191. async applyColor(params: StructureComponentManager.ColorParams, structures?: ReadonlyArray<StructureRef>) {
  192. return this.plugin.dataTransaction(async () => {
  193. const xs = structures || this.currentStructures;
  194. if (xs.length === 0) return;
  195. const getLoci = (s: Structure) => this.plugin.managers.structure.selection.getLoci(s);
  196. for (const s of xs) {
  197. if (params.action.name === 'reset') {
  198. await clearStructureOverpaint(this.plugin, s.components, params.representations);
  199. } else {
  200. const p = params.action.params;
  201. await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations, p.opacity);
  202. }
  203. }
  204. });
  205. }
  206. private updateComponent(builder: StateBuilder.Root, component: StructureComponentRef, by: Structure, action: 'union' | 'subtract') {
  207. const structure = component.cell.obj?.data;
  208. if (!structure) return;
  209. if (action === 'subtract' && !structureAreIntersecting(structure, by)) return;
  210. const parent = component.structure.cell.obj?.data!;
  211. const modified = action === 'union' ? structureUnion(parent, [structure, by]) : structureSubtract(structure, by);
  212. if (modified.elementCount === 0) {
  213. builder.delete(component.cell.transform.ref);
  214. } else {
  215. const bundle = StructureElement.Bundle.fromSubStructure(parent, modified);
  216. const params: StructureComponentParams = {
  217. type: { name: 'bundle', params: bundle },
  218. nullIfEmpty: true,
  219. label: component.cell.obj?.label!
  220. };
  221. builder.to(component.cell).update(params)
  222. }
  223. }
  224. private get dataState() {
  225. return this.plugin.state.dataState;
  226. }
  227. private clearComponents(structures: ReadonlyArray<StructureRef>) {
  228. const deletes = this.dataState.build();
  229. for (const s of structures) {
  230. for (const c of s.components) {
  231. deletes.delete(c.cell.transform.ref);
  232. }
  233. if (s.currentFocus) {
  234. if (s.currentFocus.focus) deletes.delete(s.currentFocus.focus.cell.transform.ref);
  235. if (s.currentFocus.surroundings) deletes.delete(s.currentFocus.surroundings.cell.transform.ref);
  236. }
  237. }
  238. return this.plugin.runTask(this.dataState.updateTree(deletes));
  239. }
  240. constructor(public plugin: PluginContext) {
  241. super({ options: PD.getDefaultValues(StructureComponentManager.OptionsParams) })
  242. }
  243. }
  244. namespace StructureComponentManager {
  245. export const OptionsParams = {
  246. showHydrogens: PD.Boolean(true, { description: 'Toggle display of hydrogen atoms in representations' }),
  247. visualQuality: PD.Select('auto', VisualQualityOptions, { description: 'Control the visual/rendering quality of representations' }),
  248. interactions: PD.Group(InteractionsProvider.defaultParams, { label: 'Non-covalent Interactions' }),
  249. }
  250. export type Options = PD.Values<typeof OptionsParams>
  251. const SelectionParam = PD.Select(StructureSelectionQueryOptions[1][0], StructureSelectionQueryOptions)
  252. export function getAddParams(plugin: PluginContext) {
  253. return {
  254. selection: SelectionParam,
  255. representation: getRepresentationTypesSelect(plugin, plugin.managers.structure.component.pivotStructure, [['none', '< None >']]),
  256. label: PD.Text('')
  257. };
  258. }
  259. export type AddParams = { selection: StructureSelectionQuery, label: string, representation: string }
  260. export function getColorParams(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
  261. return {
  262. action: PD.MappedStatic('color', {
  263. color: PD.Group({
  264. color: PD.Color(ColorNames.blue, { isExpanded: true }),
  265. opacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
  266. }, { isFlat: true }),
  267. reset: PD.EmptyGroup()
  268. }),
  269. representations: PD.MultiSelect([], getRepresentationTypes(plugin, pivot), { emptyValue: 'All' })
  270. };
  271. }
  272. export type ColorParams = PD.Values<ReturnType<typeof getColorParams>>
  273. export function getRepresentationTypes(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
  274. return pivot?.cell.obj?.data
  275. ? plugin.structureRepresentation.registry.getApplicableTypes(pivot.cell.obj?.data!)
  276. : plugin.structureRepresentation.registry.types;
  277. }
  278. function getRepresentationTypesSelect(plugin: PluginContext, pivot: StructureRef | undefined, custom: [string, string][], label?: string) {
  279. const types = [
  280. ...custom,
  281. ...getRepresentationTypes(plugin, pivot)
  282. ] as [string, string][];
  283. return PD.Select(types[0][0], types, { label });
  284. }
  285. }