measurement.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /**
  2. * Copyright (c) 2019-2021 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 { StructureElement } from '../../../mol-model/structure';
  8. import { PluginContext } from '../../../mol-plugin/context';
  9. import { StateSelection, StateTransform, StateTransformer, StateObject, StateObjectCell } from '../../../mol-state';
  10. import { StateTransforms } from '../../transforms';
  11. import { PluginCommands } from '../../../mol-plugin/commands';
  12. import { arraySetAdd } from '../../../mol-util/array';
  13. import { PluginStateObject } from '../../objects';
  14. import { StatefulPluginComponent } from '../../component';
  15. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  16. import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common';
  17. import { LineParams } from '../../../mol-repr/structure/representation/line';
  18. import { Expression } from '../../../mol-script/language/expression';
  19. import { Color } from '../../../mol-util/color';
  20. export { StructureMeasurementManager };
  21. export const MeasurementGroupTag = 'measurement-group';
  22. export const MeasurementOrderLabelTag = 'measurement-order-label';
  23. export type StructureMeasurementCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>>
  24. export const StructureMeasurementParams = {
  25. distanceUnitLabel: PD.Text('\u212B', { isEssential: true }),
  26. textColor: MeasurementRepresentationCommonTextParams.textColor
  27. };
  28. const DefaultStructureMeasurementOptions = PD.getDefaultValues(StructureMeasurementParams);
  29. export type StructureMeasurementOptions = PD.ValuesFor<typeof StructureMeasurementParams>
  30. export interface StructureMeasurementManagerState {
  31. labels: StructureMeasurementCell[],
  32. distances: StructureMeasurementCell[],
  33. angles: StructureMeasurementCell[],
  34. dihedrals: StructureMeasurementCell[],
  35. orientations: StructureMeasurementCell[],
  36. planes: StructureMeasurementCell[],
  37. options: StructureMeasurementOptions
  38. }
  39. type StructureMeasurementManagerAddOptions = {
  40. customText?: string,
  41. selectionTags?: string | string[],
  42. reprTags?: string | string[],
  43. lineParams?: Partial<PD.Values<LineParams>>,
  44. labelParams?: Partial<PD.Values<LociLabelTextParams>>
  45. }
  46. class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasurementManagerState> {
  47. readonly behaviors = {
  48. state: this.ev.behavior(this.state)
  49. };
  50. private stateUpdated() {
  51. this.behaviors.state.next(this.state);
  52. }
  53. private getGroup() {
  54. const state = this.plugin.state.data;
  55. const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
  56. const builder = this.plugin.state.data.build();
  57. if (groupRef) return builder.to(groupRef);
  58. return builder.toRoot().group(StateTransforms.Misc.CreateGroup, { label: `Measurements` }, { tags: MeasurementGroupTag });
  59. }
  60. async setOptions(options: StructureMeasurementOptions) {
  61. if (this.updateState({ options })) this.stateUpdated();
  62. const update = this.plugin.state.data.build();
  63. for (const cell of this.state.distances) {
  64. update.to(cell).update((old: any) => {
  65. old.unitLabel = options.distanceUnitLabel;
  66. old.textColor = options.textColor;
  67. });
  68. }
  69. for (const cell of this.state.labels) {
  70. update.to(cell).update((old: any) => { old.textColor = options.textColor; });
  71. }
  72. for (const cell of this.state.angles) {
  73. update.to(cell).update((old: any) => { old.textColor = options.textColor; });
  74. }
  75. for (const cell of this.state.dihedrals) {
  76. update.to(cell).update((old: any) => { old.textColor = options.textColor; });
  77. }
  78. if (update.editInfo.count === 0) return;
  79. await PluginCommands.State.Update(this.plugin, { state: this.plugin.state.data, tree: update, options: { doNotLogTiming: true } });
  80. }
  81. async addDistance(a: StructureElement.Loci, b: StructureElement.Loci,
  82. options?: StructureMeasurementManagerAddOptions & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsDistance3D>> }) {
  83. const cellA = this.plugin.helpers.substructureParent.get(a.structure);
  84. const cellB = this.plugin.helpers.substructureParent.get(b.structure);
  85. if (!cellA || !cellB) return;
  86. const dependsOn = [cellA.transform.ref];
  87. arraySetAdd(dependsOn, cellB.transform.ref);
  88. const update = this.getGroup();
  89. update
  90. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  91. selections: [
  92. { key: 'a', groupId: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
  93. { key: 'b', groupId: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) }
  94. ],
  95. isTransitive: true,
  96. label: 'Distance'
  97. }, { dependsOn, tags: options?.selectionTags })
  98. .apply(StateTransforms.Representation.StructureSelectionsDistance3D, {
  99. customText: options?.customText || '',
  100. unitLabel: this.state.options.distanceUnitLabel,
  101. textColor: this.state.options.textColor,
  102. ...(options?.lineParams as any),
  103. ...options?.labelParams,
  104. ...options?.visualParams
  105. }, { tags: options?.reprTags });
  106. const state = this.plugin.state.data;
  107. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  108. }
  109. async addAngle(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci,
  110. options?: StructureMeasurementManagerAddOptions & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsAngle3D>> }) {
  111. const cellA = this.plugin.helpers.substructureParent.get(a.structure);
  112. const cellB = this.plugin.helpers.substructureParent.get(b.structure);
  113. const cellC = this.plugin.helpers.substructureParent.get(c.structure);
  114. if (!cellA || !cellB || !cellC) return;
  115. const dependsOn = [cellA.transform.ref];
  116. arraySetAdd(dependsOn, cellB.transform.ref);
  117. arraySetAdd(dependsOn, cellC.transform.ref);
  118. const update = this.getGroup();
  119. update
  120. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  121. selections: [
  122. { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
  123. { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
  124. { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) }
  125. ],
  126. isTransitive: true,
  127. label: 'Angle'
  128. }, { dependsOn, tags: options?.selectionTags })
  129. .apply(StateTransforms.Representation.StructureSelectionsAngle3D, {
  130. customText: options?.customText || '',
  131. textColor: this.state.options.textColor,
  132. ...(options?.lineParams as any),
  133. ...options?.labelParams,
  134. ...options?.visualParams
  135. }, { tags: options?.reprTags });
  136. const state = this.plugin.state.data;
  137. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  138. }
  139. async addDihedral(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, d: StructureElement.Loci,
  140. options?: StructureMeasurementManagerAddOptions & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsDihedral3D>> }) {
  141. const cellA = this.plugin.helpers.substructureParent.get(a.structure);
  142. const cellB = this.plugin.helpers.substructureParent.get(b.structure);
  143. const cellC = this.plugin.helpers.substructureParent.get(c.structure);
  144. const cellD = this.plugin.helpers.substructureParent.get(d.structure);
  145. if (!cellA || !cellB || !cellC || !cellD) return;
  146. const dependsOn = [cellA.transform.ref];
  147. arraySetAdd(dependsOn, cellB.transform.ref);
  148. arraySetAdd(dependsOn, cellC.transform.ref);
  149. arraySetAdd(dependsOn, cellD.transform.ref);
  150. const update = this.getGroup();
  151. update
  152. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  153. selections: [
  154. { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
  155. { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
  156. { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) },
  157. { key: 'd', ref: cellD.transform.ref, expression: StructureElement.Loci.toExpression(d) }
  158. ],
  159. isTransitive: true,
  160. label: 'Dihedral'
  161. }, { dependsOn, tags: options?.selectionTags })
  162. .apply(StateTransforms.Representation.StructureSelectionsDihedral3D, {
  163. customText: options?.customText || '',
  164. textColor: this.state.options.textColor,
  165. ...(options?.lineParams as any),
  166. ...options?.labelParams,
  167. ...options?.visualParams
  168. }, { tags: options?.reprTags });
  169. const state = this.plugin.state.data;
  170. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  171. }
  172. async addLabel(a: StructureElement.Loci,
  173. options?: Omit<StructureMeasurementManagerAddOptions, 'customText' | 'lineParams'> & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsLabel3D>> }) {
  174. const cellA = this.plugin.helpers.substructureParent.get(a.structure);
  175. if (!cellA) return;
  176. const dependsOn = [cellA.transform.ref];
  177. const update = this.getGroup();
  178. update
  179. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  180. selections: [
  181. { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
  182. ],
  183. isTransitive: true,
  184. label: 'Label'
  185. }, { dependsOn, tags: options?.selectionTags })
  186. .apply(StateTransforms.Representation.StructureSelectionsLabel3D, {
  187. textColor: this.state.options.textColor,
  188. ...options?.labelParams,
  189. ...options?.visualParams
  190. }, { tags: options?.reprTags });
  191. const state = this.plugin.state.data;
  192. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  193. }
  194. async addOrientation(locis: StructureElement.Loci[]) {
  195. const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = [];
  196. const dependsOn: string[] = [];
  197. for (let i = 0, il = locis.length; i < il; ++i) {
  198. const l = locis[i];
  199. const cell = this.plugin.helpers.substructureParent.get(l.structure);
  200. if (!cell) continue;
  201. arraySetAdd(dependsOn, cell.transform.ref);
  202. selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) });
  203. }
  204. if (selections.length === 0) return;
  205. const update = this.getGroup();
  206. update
  207. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  208. selections,
  209. isTransitive: true,
  210. label: 'Orientation'
  211. }, { dependsOn })
  212. .apply(StateTransforms.Representation.StructureSelectionsOrientation3D);
  213. const state = this.plugin.state.data;
  214. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  215. }
  216. async addPlane(locis: StructureElement.Loci[]) {
  217. const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = [];
  218. const dependsOn: string[] = [];
  219. for (let i = 0, il = locis.length; i < il; ++i) {
  220. const l = locis[i];
  221. const cell = this.plugin.helpers.substructureParent.get(l.structure);
  222. if (!cell) continue;
  223. arraySetAdd(dependsOn, cell.transform.ref);
  224. selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) });
  225. }
  226. if (selections.length === 0) return;
  227. const update = this.getGroup();
  228. update
  229. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  230. selections,
  231. isTransitive: true,
  232. label: 'Plane'
  233. }, { dependsOn })
  234. .apply(StateTransforms.Representation.StructureSelectionsPlane3D);
  235. const state = this.plugin.state.data;
  236. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  237. }
  238. async addOrderLabels(locis: StructureElement.Loci[]) {
  239. const update = this.getGroup();
  240. const current = this.plugin.state.data.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Selections).withTag(MeasurementOrderLabelTag));
  241. for (const obj of current)
  242. update.delete(obj);
  243. let order = 1;
  244. for (const loci of locis) {
  245. const cell = this.plugin.helpers.substructureParent.get(loci.structure);
  246. if (!cell) continue;
  247. const dependsOn = [cell.transform.ref];
  248. update
  249. .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
  250. selections: [
  251. { key: 'a', ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(loci) },
  252. ],
  253. isTransitive: true,
  254. label: 'Order'
  255. }, { dependsOn, tags: MeasurementOrderLabelTag })
  256. .apply(StateTransforms.Representation.StructureSelectionsLabel3D, {
  257. textColor: Color.fromRgb(255, 255, 255),
  258. borderColor: Color.fromRgb(0, 0, 0),
  259. borderWidth: 0.5,
  260. textSize: 0.33,
  261. customText: `${order++}`
  262. }, { tags: MeasurementOrderLabelTag });
  263. }
  264. const state = this.plugin.state.data;
  265. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
  266. }
  267. private _empty: any[] = [];
  268. private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) {
  269. const state = this.plugin.state.data;
  270. const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
  271. const ret = groupRef ? state.select(StateSelection.Generators.ofTransformer(transformer, groupRef)) : this._empty;
  272. if (ret.length === 0) return this._empty;
  273. return ret;
  274. }
  275. private sync() {
  276. const labels = [];
  277. for (const cell of this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D) as StructureMeasurementCell[]) {
  278. const tags = (cell.obj as any)['tags'] as string[];
  279. if (!tags || !tags.includes(MeasurementOrderLabelTag))
  280. labels.push(cell);
  281. }
  282. const updated = this.updateState({
  283. labels,
  284. distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D),
  285. angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D),
  286. dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D),
  287. orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D),
  288. planes: this.getTransforms(StateTransforms.Representation.StructureSelectionsPlane3D),
  289. });
  290. if (updated) this.stateUpdated();
  291. }
  292. constructor(private plugin: PluginContext) {
  293. super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], planes: [], options: DefaultStructureMeasurementOptions });
  294. plugin.state.data.events.changed.subscribe(e => {
  295. if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return;
  296. this.sync();
  297. });
  298. plugin.behaviors.state.isAnimating.subscribe(isAnimating => {
  299. if (!isAnimating && !plugin.behaviors.state.isUpdating.value) this.sync();
  300. });
  301. }
  302. }