component.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /**
  2. * Copyright (c) 2019-2020 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, structureIntersect, 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 { ColorTheme } from '../../../mol-theme/color';
  15. import { SizeTheme } from '../../../mol-theme/size';
  16. import { UUID } from '../../../mol-util';
  17. import { ColorNames } from '../../../mol-util/color/names';
  18. import { objectForEach } from '../../../mol-util/object';
  19. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  20. import { StructureRepresentationPresetProvider } from '../../builder/structure/representation-preset';
  21. import { StatefulPluginComponent } from '../../component';
  22. import { StructureComponentParams } from '../../helpers/structure-component';
  23. import { clearStructureOverpaint, setStructureOverpaint } from '../../helpers/structure-overpaint';
  24. import { createStructureColorThemeParams, createStructureSizeThemeParams } from '../../helpers/structure-representation-params';
  25. import { StructureSelectionQueries, StructureSelectionQuery } from '../../helpers/structure-selection-query';
  26. import { StructureRepresentation3D } from '../../transforms/representation';
  27. import { HierarchyRef, StructureComponentRef, StructureRef, StructureRepresentationRef } from './hierarchy-state';
  28. export { StructureComponentManager };
  29. interface StructureComponentManagerState {
  30. options: StructureComponentManager.Options
  31. }
  32. class StructureComponentManager extends StatefulPluginComponent<StructureComponentManagerState> {
  33. readonly events = {
  34. optionsUpdated: this.ev<undefined>()
  35. }
  36. get currentStructures() {
  37. return this.plugin.managers.structure.hierarchy.selection.structures;
  38. }
  39. get pivotStructure(): StructureRef | undefined {
  40. return this.currentStructures[0];
  41. }
  42. async setOptions(options: StructureComponentManager.Options) {
  43. const interactionChanged = options.interactions !== this.state.options.interactions;
  44. this.updateState({ options });
  45. this.events.optionsUpdated.next();
  46. const update = this.dataState.build();
  47. for (const s of this.currentStructures) {
  48. for (const c of s.components) {
  49. this.updateReprParams(update, c);
  50. }
  51. }
  52. return this.plugin.dataTransaction(async () => {
  53. await update.commit();
  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 oldParams = s.properties.cell.transform.params?.properties[InteractionsProvider.descriptor.name];
  76. if (PD.areEqual(interactionParams, oldParams, this.state.options.interactions)) continue;
  77. await this.dataState.build().to(s.properties.cell)
  78. .update(old => {
  79. old.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
  80. })
  81. .commit();
  82. } else {
  83. const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
  84. const params = PD.getDefaultValues(pd);
  85. if (PD.areEqual(interactionParams, params.properties[InteractionsProvider.descriptor.name], this.state.options.interactions)) continue;
  86. params.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
  87. await this.plugin.builders.structure.insertStructureProperties(s.cell, params);
  88. }
  89. }
  90. }
  91. applyPreset<P extends StructureRepresentationPresetProvider>(structures: ReadonlyArray<StructureRef>, provider: P, params?: StructureRepresentationPresetProvider.Params<P>): Promise<any> {
  92. return this.plugin.dataTransaction(async () => {
  93. for (const s of structures) {
  94. const preset = await this.plugin.builders.structure.representation.applyPreset(s.cell, provider, params);
  95. await this.syncPreset(s, preset);
  96. }
  97. }, { canUndo: 'Preset' });
  98. }
  99. private syncPreset(root: StructureRef, preset?: StructureRepresentationPresetProvider.Result) {
  100. if (!preset || !preset.components) return this.clearComponents([root]);
  101. const keptRefs = new Set<string>();
  102. objectForEach(preset.components, c => {
  103. if (c) keptRefs.add(c.ref);
  104. });
  105. if (preset.representations) {
  106. objectForEach(preset.representations, r => {
  107. if (r) keptRefs.add(r.ref);
  108. });
  109. }
  110. if (keptRefs.size === 0) return this.clearComponents([root]);
  111. let changed = false;
  112. const update = this.dataState.build();
  113. const sync = (r: HierarchyRef) => {
  114. if (!keptRefs.has(r.cell.transform.ref)) {
  115. changed = true;
  116. update.delete(r.cell);
  117. }
  118. };
  119. for (const c of root.components) {
  120. sync(c);
  121. for (const r of c.representations) sync(r);
  122. if (c.genericRepresentations) {
  123. for (const r of c.genericRepresentations) sync(r);
  124. }
  125. }
  126. if (root.genericRepresentations) {
  127. for (const r of root.genericRepresentations) {
  128. sync(r);
  129. }
  130. }
  131. if (changed) return update.commit();
  132. }
  133. clear(structures: ReadonlyArray<StructureRef>) {
  134. return this.clearComponents(structures);
  135. }
  136. selectThis(components: ReadonlyArray<StructureComponentRef>) {
  137. const mng = this.plugin.managers.structure.selection;
  138. mng.clear();
  139. for (const c of components) {
  140. const loci = Structure.toSubStructureElementLoci(c.structure.cell.obj!.data, c.cell.obj?.data!)
  141. mng.fromLoci('set', loci);
  142. }
  143. }
  144. canBeModified(ref: HierarchyRef) {
  145. return this.plugin.builders.structure.isComponentTransform(ref.cell);
  146. }
  147. modifyByCurrentSelection(components: ReadonlyArray<StructureComponentRef>, action: StructureComponentManager.ModifyAction) {
  148. return this.plugin.runTask(Task.create('Modify Component', async taskCtx => {
  149. const b = this.dataState.build();
  150. for (const c of components) {
  151. if (!this.canBeModified(c)) continue;
  152. const selection = this.plugin.managers.structure.selection.getStructure(c.structure.cell.obj!.data);
  153. if (!selection || selection.elementCount === 0) continue;
  154. this.modifyComponent(b, c, selection, action);
  155. }
  156. await this.dataState.updateTree(b, { canUndo: 'Modify Selection' }).runInContext(taskCtx);
  157. }));
  158. }
  159. toggleVisibility(components: ReadonlyArray<StructureComponentRef>, reprPivot?: StructureRepresentationRef) {
  160. if (components.length === 0) return;
  161. if (!reprPivot) {
  162. const isHidden = !components[0].cell.state.isHidden;
  163. for (const c of components) {
  164. setSubtreeVisibility(this.dataState, c.cell.transform.ref, isHidden);
  165. }
  166. } else {
  167. const index = components[0].representations.indexOf(reprPivot);
  168. const isHidden = !reprPivot.cell.state.isHidden;
  169. for (const c of components) {
  170. // TODO: is it ok to use just the index here? Could possible lead to ugly edge cases, but perhaps not worth the trouble to "fix".
  171. const repr = c.representations[index];
  172. if (!repr) continue;
  173. setSubtreeVisibility(this.dataState, repr.cell.transform.ref, isHidden)
  174. }
  175. }
  176. }
  177. removeRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot?: StructureRepresentationRef) {
  178. if (components.length === 0) return;
  179. const toRemove: HierarchyRef[] = [];
  180. if (pivot) {
  181. const index = components[0].representations.indexOf(pivot);
  182. if (index < 0) return;
  183. for (const c of components) {
  184. if (c.representations[index]) toRemove.push(c.representations[index]);
  185. }
  186. } else {
  187. for (const c of components) {
  188. for (const r of c.representations) {
  189. toRemove.push(r);
  190. }
  191. }
  192. }
  193. return this.plugin.managers.structure.hierarchy.remove(toRemove, true);
  194. }
  195. updateRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot: StructureRepresentationRef, params: StateTransformer.Params<StructureRepresentation3D>) {
  196. if (components.length === 0) return Promise.resolve();
  197. const index = components[0].representations.indexOf(pivot);
  198. if (index < 0) return Promise.resolve();
  199. const update = this.dataState.build();
  200. for (const c of components) {
  201. // TODO: is it ok to use just the index here? Could possible lead to ugly edge cases, but perhaps not worth the trouble to "fix".
  202. const repr = c.representations[index];
  203. if (!repr) continue;
  204. if (repr.cell.transform.transformer !== pivot.cell.transform.transformer) continue;
  205. update.to(repr.cell).update(params);
  206. }
  207. return update.commit({ canUndo: 'Update Representation' });
  208. }
  209. /**
  210. * To update theme for all selected structures, use
  211. * plugin.dataTransaction(async () => {
  212. * for (const s of structure.hierarchy.selection.structures) await updateRepresentationsTheme(s.componets, ...);
  213. * }, { canUndo: 'Update Theme' });
  214. */
  215. updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> | undefined
  216. updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: (c: StructureComponentRef, r: StructureRepresentationRef) => StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> | undefined
  217. updateRepresentationsTheme(components: ReadonlyArray<StructureComponentRef>, paramsOrProvider: StructureComponentManager.UpdateThemeParams<any, any> | ((c: StructureComponentRef, r: StructureRepresentationRef) => StructureComponentManager.UpdateThemeParams<any, any>)) {
  218. if (components.length === 0) return;
  219. const update = this.dataState.build();
  220. for (const c of components) {
  221. for (const repr of c.representations) {
  222. const old = repr.cell.transform.params;
  223. const params: StructureComponentManager.UpdateThemeParams<any, any> = typeof paramsOrProvider === 'function' ? paramsOrProvider(c, repr) : paramsOrProvider;
  224. const colorTheme = params.color === 'default'
  225. ? createStructureColorThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name)
  226. : params.color
  227. ? createStructureColorThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.color, params.colorParams)
  228. : void 0;
  229. const sizeTheme = params.size === 'default'
  230. ? createStructureSizeThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name)
  231. : params.color
  232. ? createStructureSizeThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.size, params.sizeParams)
  233. : void 0;
  234. if (colorTheme || sizeTheme) {
  235. update.to(repr.cell).update(prev => {
  236. if (colorTheme) prev.colorTheme = colorTheme;
  237. if (sizeTheme) prev.sizeTheme = sizeTheme;
  238. });
  239. }
  240. }
  241. }
  242. return update.commit({ canUndo: 'Update Theme' });
  243. }
  244. addRepresentation(components: ReadonlyArray<StructureComponentRef>, type: string) {
  245. if (components.length === 0) return;
  246. const { showHydrogens, visualQuality: quality } = this.state.options;
  247. const ignoreHydrogens = !showHydrogens;
  248. const typeParams = { ignoreHydrogens, quality };
  249. return this.plugin.dataTransaction(async () => {
  250. for (const component of components) {
  251. await this.plugin.builders.structure.representation.addRepresentation(component.cell, {
  252. type: this.plugin.representation.structure.registry.get(type),
  253. typeParams
  254. });
  255. }
  256. }, { canUndo: 'Add Representation' });
  257. }
  258. async add(params: StructureComponentManager.AddParams, structures?: ReadonlyArray<StructureRef>) {
  259. return this.plugin.dataTransaction(async () => {
  260. const xs = structures || this.currentStructures;
  261. if (xs.length === 0) return;
  262. const { showHydrogens, visualQuality: quality } = this.state.options;
  263. const ignoreHydrogens = !showHydrogens;
  264. const typeParams = { ignoreHydrogens, quality };
  265. const componentKey = UUID.create22();
  266. for (const s of xs) {
  267. const component = await this.plugin.builders.structure.tryCreateComponentFromSelection(s.cell, params.selection, componentKey, {
  268. label: params.label || (params.selection === StructureSelectionQueries.current ? 'Custom Selection' : ''),
  269. });
  270. if (params.representation === 'none' || !component) continue;
  271. await this.plugin.builders.structure.representation.addRepresentation(component, {
  272. type: this.plugin.representation.structure.registry.get(params.representation),
  273. typeParams
  274. });
  275. }
  276. }, { canUndo: 'Add Selection' });
  277. }
  278. async applyColor(params: StructureComponentManager.ColorParams, structures?: ReadonlyArray<StructureRef>) {
  279. return this.plugin.dataTransaction(async () => {
  280. const xs = structures || this.currentStructures;
  281. if (xs.length === 0) return;
  282. const getLoci = (s: Structure) => this.plugin.managers.structure.selection.getLoci(s);
  283. for (const s of xs) {
  284. if (params.action.name === 'reset') {
  285. await clearStructureOverpaint(this.plugin, s.components, params.representations);
  286. } else {
  287. const p = params.action.params;
  288. await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations, p.opacity);
  289. }
  290. }
  291. }, { canUndo: 'Apply Color' });
  292. }
  293. private modifyComponent(builder: StateBuilder.Root, component: StructureComponentRef, by: Structure, action: StructureComponentManager.ModifyAction) {
  294. const structure = component.cell.obj?.data;
  295. if (!structure) return;
  296. if ((action === 'subtract' || action === 'intersect') && !structureAreIntersecting(structure, by)) return;
  297. const parent = component.structure.cell.obj?.data!;
  298. const modified = action === 'union'
  299. ? structureUnion(parent, [structure, by])
  300. : action === 'intersect'
  301. ? structureIntersect(structure, by)
  302. : structureSubtract(structure, by);
  303. if (modified.elementCount === 0) {
  304. builder.delete(component.cell.transform.ref);
  305. } else {
  306. const bundle = StructureElement.Bundle.fromSubStructure(parent, modified);
  307. const params: StructureComponentParams = {
  308. type: { name: 'bundle', params: bundle },
  309. nullIfEmpty: true,
  310. label: component.cell.obj?.label!
  311. };
  312. builder.to(component.cell).update(params)
  313. }
  314. }
  315. private get dataState() {
  316. return this.plugin.state.data;
  317. }
  318. private clearComponents(structures: ReadonlyArray<StructureRef>) {
  319. const deletes = this.dataState.build();
  320. for (const s of structures) {
  321. for (const c of s.components) {
  322. deletes.delete(c.cell.transform.ref);
  323. }
  324. }
  325. return deletes.commit({ canUndo: 'Clear Selections' });
  326. }
  327. constructor(public plugin: PluginContext) {
  328. super({ options: PD.getDefaultValues(StructureComponentManager.OptionsParams) })
  329. }
  330. }
  331. namespace StructureComponentManager {
  332. export const OptionsParams = {
  333. showHydrogens: PD.Boolean(true, { description: 'Toggle display of hydrogen atoms in representations' }),
  334. visualQuality: PD.Select('auto', VisualQualityOptions, { description: 'Control the visual/rendering quality of representations' }),
  335. interactions: PD.Group(InteractionsProvider.defaultParams, { label: 'Non-covalent Interactions' }),
  336. }
  337. export type Options = PD.Values<typeof OptionsParams>
  338. export function getAddParams(plugin: PluginContext) {
  339. const { options } = plugin.query.structure.registry
  340. return {
  341. selection: PD.Select(options[1][0], options),
  342. representation: getRepresentationTypesSelect(plugin, plugin.managers.structure.component.pivotStructure, [['none', '< Create Later >']]),
  343. label: PD.Text('')
  344. };
  345. }
  346. export type AddParams = { selection: StructureSelectionQuery, label: string, representation: string }
  347. export function getColorParams(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
  348. return {
  349. action: PD.MappedStatic('color', {
  350. color: PD.Group({
  351. color: PD.Color(ColorNames.blue, { isExpanded: true }),
  352. opacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
  353. }, { isFlat: true }),
  354. reset: PD.EmptyGroup()
  355. }),
  356. representations: PD.MultiSelect([], getRepresentationTypes(plugin, pivot), { emptyValue: 'All' })
  357. };
  358. }
  359. export type ColorParams = PD.Values<ReturnType<typeof getColorParams>>
  360. export function getRepresentationTypes(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
  361. return pivot?.cell.obj?.data
  362. ? plugin.representation.structure.registry.getApplicableTypes(pivot.cell.obj?.data!)
  363. : plugin.representation.structure.registry.types;
  364. }
  365. function getRepresentationTypesSelect(plugin: PluginContext, pivot: StructureRef | undefined, custom: [string, string][], label?: string) {
  366. const types = [
  367. ...custom,
  368. ...getRepresentationTypes(plugin, pivot)
  369. ] as [string, string][];
  370. return PD.Select(types[0][0], types, { label });
  371. }
  372. export type ModifyAction = 'union' | 'subtract' | 'intersect'
  373. export interface UpdateThemeParams<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn> {
  374. /**
  375. * this works for any theme name (use 'name as any'), but code completion will break
  376. */
  377. color?: C | 'default',
  378. colorParams?: ColorTheme.BuiltInParams<C>,
  379. size?: S | 'default',
  380. sizeParams?: SizeTheme.BuiltInParams<S>
  381. }
  382. }