state.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /**
  2. * Copyright (c) 2018-2023 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 { Structure } from '../../../mol-model/structure';
  8. import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
  9. import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
  10. import { State, StateTransform, StateTree } from '../../../mol-state';
  11. import { getFormattedTime } from '../../../mol-util/date';
  12. import { download } from '../../../mol-util/download';
  13. import { urlCombine } from '../../../mol-util/url';
  14. import { PluginCommands } from '../../commands';
  15. import { PluginConfig } from '../../config';
  16. import { PluginContext } from '../../context';
  17. export function registerDefault(ctx: PluginContext) {
  18. SyncBehaviors(ctx);
  19. SetCurrentObject(ctx);
  20. Update(ctx);
  21. ApplyAction(ctx);
  22. RemoveObject(ctx);
  23. ToggleExpanded(ctx);
  24. ToggleVisibility(ctx);
  25. Highlight(ctx);
  26. ClearHighlights(ctx);
  27. Snapshots(ctx);
  28. }
  29. export function SyncBehaviors(ctx: PluginContext) {
  30. ctx.state.events.object.created.subscribe(o => {
  31. if (!SO.isBehavior(o.obj)) return;
  32. o.obj.data.register(o.ref);
  33. });
  34. ctx.state.events.object.removed.subscribe(o => {
  35. if (!SO.isBehavior(o.obj)) return;
  36. o.obj.data.unregister?.();
  37. o.obj.data.dispose?.();
  38. });
  39. ctx.state.events.object.updated.subscribe(o => {
  40. if (o.action === 'recreate') {
  41. if (o.oldObj && SO.isBehavior(o.oldObj)) {
  42. o.oldObj.data.unregister?.();
  43. o.oldObj.data.dispose?.();
  44. }
  45. if (o.obj && SO.isBehavior(o.obj)) o.obj.data.register(o.ref);
  46. }
  47. });
  48. }
  49. export function SetCurrentObject(ctx: PluginContext) {
  50. PluginCommands.State.SetCurrentObject.subscribe(ctx, ({ state, ref }) => state.setCurrent(ref));
  51. }
  52. export function Update(ctx: PluginContext) {
  53. PluginCommands.State.Update.subscribe(ctx, ({ state, tree, options }) => ctx.runTask(state.updateTree(tree, options)));
  54. }
  55. export function ApplyAction(ctx: PluginContext) {
  56. PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.applyAction(action.action, action.params, ref)));
  57. }
  58. export function RemoveObject(ctx: PluginContext) {
  59. function remove(state: State, ref: string) {
  60. const tree = state.build().delete(ref);
  61. return ctx.runTask(state.updateTree(tree));
  62. }
  63. PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref, removeParentGhosts }) => {
  64. if (removeParentGhosts) {
  65. const tree = state.tree;
  66. let curr = tree.transforms.get(ref);
  67. if (curr.parent === ref) return remove(state, ref);
  68. while (true) {
  69. const children = tree.children.get(curr.parent);
  70. if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref);
  71. const parent = tree.transforms.get(curr.parent);
  72. // TODO: should this use "cell state" instead?
  73. if (!parent.state.isGhost) return remove(state, curr.ref);
  74. curr = parent;
  75. }
  76. } else {
  77. return remove(state, ref);
  78. }
  79. });
  80. }
  81. export function ToggleExpanded(ctx: PluginContext) {
  82. PluginCommands.State.ToggleExpanded.subscribe(ctx, ({ state, ref }) => state.updateCellState(ref, ({ isCollapsed }) => ({ isCollapsed: !isCollapsed })));
  83. }
  84. export function ToggleVisibility(ctx: PluginContext) {
  85. PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setSubtreeVisibility(state, ref, !state.cells.get(ref)!.state.isHidden));
  86. }
  87. export function setSubtreeVisibility(state: State, root: StateTransform.Ref, value: boolean) {
  88. StateTree.doPreOrder(state.tree, state.transforms.get(root), { state, value }, setVisibilityVisitor);
  89. }
  90. function setVisibilityVisitor(t: StateTransform, tree: StateTree, ctx: { state: State, value: boolean }) {
  91. ctx.state.updateCellState(t.ref, { isHidden: ctx.value });
  92. }
  93. export function Highlight(ctx: PluginContext) {
  94. PluginCommands.Interactivity.Object.Highlight.subscribe(ctx, ({ state, ref }) => {
  95. if (!ctx.canvas3d || ctx.isBusy) return;
  96. ctx.managers.interactivity.lociHighlights.clearHighlights();
  97. const refs = typeof ref === 'string' ? [ref] : ref;
  98. for (const r of refs) {
  99. const cell = state.cells.get(r);
  100. if (!cell) continue;
  101. if (SO.Molecule.Structure.is(cell.obj)) {
  102. ctx.managers.interactivity.lociHighlights.highlight({ loci: Structure.Loci(cell.obj.data) }, false);
  103. } else if (cell && SO.isRepresentation3D(cell.obj)) {
  104. const { repr } = cell.obj.data;
  105. for (const loci of repr.getAllLoci()) {
  106. ctx.managers.interactivity.lociHighlights.highlight({ loci, repr }, false);
  107. }
  108. } else if (SO.Molecule.Structure.Selections.is(cell.obj)) {
  109. for (const entry of cell.obj.data) {
  110. ctx.managers.interactivity.lociHighlights.highlight({ loci: entry.loci }, false);
  111. }
  112. }
  113. }
  114. // TODO: highlight volumes?
  115. // TODO: select structures of subtree?
  116. });
  117. }
  118. export function ClearHighlights(ctx: PluginContext) {
  119. PluginCommands.Interactivity.ClearHighlights.subscribe(ctx, () => {
  120. ctx.managers.interactivity.lociHighlights.clearHighlights();
  121. });
  122. }
  123. export function Snapshots(ctx: PluginContext) {
  124. ctx.config.set(PluginConfig.State.CurrentServer, ctx.config.get(PluginConfig.State.DefaultServer));
  125. PluginCommands.State.Snapshots.Clear.subscribe(ctx, () => {
  126. ctx.managers.snapshot.clear();
  127. });
  128. PluginCommands.State.Snapshots.Remove.subscribe(ctx, ({ id }) => {
  129. ctx.managers.snapshot.remove(id);
  130. });
  131. PluginCommands.State.Snapshots.Add.subscribe(ctx, async ({ name, description, params }) => {
  132. const snapshot = ctx.state.getSnapshot(params);
  133. const image = (params?.image ?? ctx.state.snapshotParams.value.image) ? await PluginStateSnapshotManager.getCanvasImageAsset(ctx, `${snapshot.id}-image.png`) : undefined;
  134. const entry = PluginStateSnapshotManager.Entry(snapshot, { name, description, image });
  135. ctx.managers.snapshot.add(entry);
  136. });
  137. PluginCommands.State.Snapshots.Replace.subscribe(ctx, async ({ id, params }) => {
  138. const snapshot = ctx.state.getSnapshot(params);
  139. const image = (params?.image ?? ctx.state.snapshotParams.value.image) ? await PluginStateSnapshotManager.getCanvasImageAsset(ctx, `${snapshot.id}-image.png`) : undefined;
  140. ctx.managers.snapshot.replace(id, ctx.state.getSnapshot(params), { image });
  141. });
  142. PluginCommands.State.Snapshots.Move.subscribe(ctx, ({ id, dir }) => {
  143. ctx.managers.snapshot.move(id, dir);
  144. });
  145. PluginCommands.State.Snapshots.Apply.subscribe(ctx, ({ id }) => {
  146. const snapshot = ctx.managers.snapshot.setCurrent(id);
  147. if (!snapshot) return;
  148. return ctx.state.setSnapshot(snapshot);
  149. });
  150. PluginCommands.State.Snapshots.Upload.subscribe(ctx, async ({ name, description, playOnLoad, serverUrl, params }) => {
  151. return fetch(urlCombine(serverUrl, `set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`), {
  152. method: 'POST',
  153. mode: 'cors',
  154. referrer: 'no-referrer',
  155. headers: { 'Content-Type': 'application/json; charset=utf-8' },
  156. body: JSON.stringify(await ctx.managers.snapshot.getStateSnapshot({ name, description, playOnLoad }))
  157. }) as unknown as Promise<void>;
  158. });
  159. PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => {
  160. const json = await ctx.runTask(ctx.fetch({ url, type: 'json' })); // fetch(url, { referrer: 'no-referrer' });
  161. await ctx.managers.snapshot.setStateSnapshot(json.data);
  162. });
  163. PluginCommands.State.Snapshots.DownloadToFile.subscribe(ctx, async ({ name, type, params }) => {
  164. const filename = `mol-star_state_${(name || getFormattedTime())}.${type === 'json' ? 'molj' : 'molx'}`;
  165. const data = await ctx.managers.snapshot.serialize({ type, params });
  166. download(data, `${filename}`);
  167. });
  168. PluginCommands.State.Snapshots.OpenFile.subscribe(ctx, ({ file }) => {
  169. return ctx.managers.snapshot.open(file);
  170. });
  171. PluginCommands.State.Snapshots.OpenUrl.subscribe(ctx, async ({ url, type }) => {
  172. const data = await ctx.runTask(ctx.fetch({ url, type: 'binary' }));
  173. return ctx.managers.snapshot.open(new File([data], `state.${type}`));
  174. });
  175. }