selection.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { PluginComponent } from '../../component';
  8. import { PluginContext } from '../../../mol-plugin/context';
  9. import { StructureElement, Structure } from '../../../mol-model/structure';
  10. import { Vec3 } from '../../../mol-math/linear-algebra';
  11. import { Boundary } from '../../../mol-model/structure/structure/util/boundary';
  12. import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
  13. import { structureElementStatsLabel } from '../../../mol-theme/label';
  14. import { OrderedSet } from '../../../mol-data/int';
  15. import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
  16. import { arrayRemoveAtInPlace } from '../../../mol-util/array';
  17. import { EmptyLoci, Loci } from '../../../mol-model/loci';
  18. import { StateObject, StateSelection } from '../../../mol-state';
  19. import { PluginStateObject } from '../../objects';
  20. import { StructureSelectionQuery } from '../../helpers/structure-selection-query';
  21. import { Task } from '../../../mol-task';
  22. interface StructureSelectionManagerState {
  23. entries: Map<string, SelectionEntry>,
  24. history: HistoryEntry[],
  25. stats?: SelectionStats
  26. }
  27. const boundaryHelper = new BoundaryHelper();
  28. const HISTORY_CAPACITY = 8;
  29. export type StructureSelectionModifier = 'add' | 'remove' | 'set'
  30. export class StructureSelectionManager extends PluginComponent<StructureSelectionManagerState> {
  31. readonly events = {
  32. changed: this.ev<undefined>()
  33. }
  34. private referenceLoci: Loci | undefined
  35. get entries() { return this.state.entries; }
  36. get history() { return this.state.history; }
  37. get stats() {
  38. if (this.state.stats) return this.state.stats;
  39. this.state.stats = this.calcStats();
  40. return this.state.stats;
  41. }
  42. private getEntry(s: Structure) {
  43. const cell = this.plugin.helpers.substructureParent.get(s);
  44. if (!cell) return;
  45. const ref = cell.transform.ref;
  46. if (!this.entries.has(ref)) {
  47. const entry = new SelectionEntry(StructureElement.Loci(s, []));
  48. this.entries.set(ref, entry);
  49. return entry;
  50. }
  51. return this.entries.get(ref)!;
  52. }
  53. private calcStats(): SelectionStats {
  54. let structureCount = 0
  55. let elementCount = 0
  56. const stats = StructureElement.Stats.create()
  57. this.entries.forEach(v => {
  58. const { elements } = v.selection
  59. if (elements.length) {
  60. structureCount += 1
  61. for (let i = 0, il = elements.length; i < il; ++i) {
  62. elementCount += OrderedSet.size(elements[i].indices)
  63. }
  64. StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(v.selection))
  65. }
  66. })
  67. const label = structureElementStatsLabel(stats, { countsOnly: true })
  68. return { structureCount, elementCount, label }
  69. }
  70. private add(loci: Loci): boolean {
  71. if (!StructureElement.Loci.is(loci)) return false;
  72. const entry = this.getEntry(loci.structure);
  73. if (!entry) return false;
  74. const sel = entry.selection;
  75. entry.selection = StructureElement.Loci.union(entry.selection, loci);
  76. this.addHistory(loci);
  77. this.referenceLoci = loci
  78. return !StructureElement.Loci.areEqual(sel, entry.selection);
  79. }
  80. private remove(loci: Loci) {
  81. if (!StructureElement.Loci.is(loci)) return false;
  82. const entry = this.getEntry(loci.structure);
  83. if (!entry) return false;
  84. const sel = entry.selection;
  85. entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
  86. this.removeHistory(loci);
  87. this.referenceLoci = loci
  88. return !StructureElement.Loci.areEqual(sel, entry.selection);
  89. }
  90. private set(loci: Loci) {
  91. if (!StructureElement.Loci.is(loci)) return false;
  92. const entry = this.getEntry(loci.structure);
  93. if (!entry) return false;
  94. const sel = entry.selection;
  95. entry.selection = loci;
  96. this.referenceLoci = undefined;
  97. return !StructureElement.Loci.areEqual(sel, entry.selection);
  98. }
  99. private addHistory(loci: StructureElement.Loci) {
  100. if (Loci.isEmpty(loci)) return;
  101. let idx = 0, entry: HistoryEntry | undefined = void 0;
  102. for (const l of this.history) {
  103. if (Loci.areEqual(l.loci, loci)) {
  104. entry = l;
  105. break;
  106. }
  107. idx++;
  108. }
  109. if (entry) {
  110. arrayRemoveAtInPlace(this.history, idx);
  111. this.history.unshift(entry);
  112. return;
  113. }
  114. const stats = StructureElement.Stats.ofLoci(loci);
  115. const label = structureElementStatsLabel(stats)
  116. this.history.unshift({ loci, label });
  117. if (this.history.length > HISTORY_CAPACITY) this.history.pop();
  118. }
  119. private removeHistory(loci: Loci) {
  120. if (Loci.isEmpty(loci)) return;
  121. let idx = 0, found = false;
  122. for (const l of this.history) {
  123. if (Loci.areEqual(l.loci, loci)) {
  124. found = true;
  125. break;
  126. }
  127. idx++;
  128. }
  129. if (found) {
  130. arrayRemoveAtInPlace(this.history, idx);
  131. }
  132. }
  133. private onRemove(ref: string) {
  134. if (this.entries.has(ref)) {
  135. this.entries.delete(ref);
  136. // TODO: property update the latest loci
  137. this.state.history = [];
  138. this.referenceLoci = undefined
  139. }
  140. }
  141. private onUpdate(ref: string, oldObj: StateObject | undefined, obj: StateObject) {
  142. if (!PluginStateObject.Molecule.Structure.is(obj)) return;
  143. if (this.entries.has(ref)) {
  144. if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
  145. // TODO: property update the latest loci & reference loci
  146. this.state.history = [];
  147. this.referenceLoci = undefined
  148. // remap the old selection to be related to the new object if possible.
  149. if (Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
  150. this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, obj.data));
  151. return;
  152. }
  153. // clear the selection
  154. this.entries.set(ref, new SelectionEntry(StructureElement.Loci(obj.data, [])));
  155. }
  156. }
  157. /** Removes all selections and returns them */
  158. clear() {
  159. const keys = this.entries.keys();
  160. const selections: StructureElement.Loci[] = [];
  161. while (true) {
  162. const k = keys.next();
  163. if (k.done) break;
  164. const s = this.entries.get(k.value)!;
  165. if (!StructureElement.Loci.isEmpty(s.selection)) selections.push(s.selection);
  166. s.selection = StructureElement.Loci(s.selection.structure, []);
  167. }
  168. this.referenceLoci = undefined
  169. this.state.stats = void 0;
  170. this.events.changed.next()
  171. return selections;
  172. }
  173. getLoci(structure: Structure) {
  174. const entry = this.getEntry(structure);
  175. if (!entry) return EmptyLoci;
  176. return entry.selection;
  177. }
  178. getStructure(structure: Structure) {
  179. const entry = this.getEntry(structure);
  180. if (!entry) return;
  181. return entry.structure;
  182. }
  183. has(loci: Loci) {
  184. if (StructureElement.Loci.is(loci)) {
  185. const entry = this.getEntry(loci.structure);
  186. if (entry) {
  187. return StructureElement.Loci.isSubset(entry.selection, loci);
  188. }
  189. }
  190. return false;
  191. }
  192. tryGetRange(loci: Loci): StructureElement.Loci | undefined {
  193. if (!StructureElement.Loci.is(loci)) return;
  194. if (loci.elements.length !== 1) return;
  195. const entry = this.getEntry(loci.structure);
  196. if (!entry) return;
  197. const xs = loci.elements[0];
  198. if (!xs) return;
  199. const ref = this.referenceLoci
  200. if (!ref || !StructureElement.Loci.is(ref) || ref.structure.root !== loci.structure.root) return;
  201. let e: StructureElement.Loci['elements'][0] | undefined;
  202. for (const _e of ref.elements) {
  203. if (xs.unit === _e.unit) {
  204. e = _e;
  205. break;
  206. }
  207. }
  208. if (!e) return;
  209. if (xs.unit !== e.unit) return;
  210. return getElementRange(loci.structure.root, e, xs)
  211. }
  212. private prevHighlight: StructureElement.Loci | undefined = void 0;
  213. accumulateInteractiveHighlight(loci: Loci) {
  214. if (StructureElement.Loci.is(loci)) {
  215. if (this.prevHighlight) {
  216. this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
  217. } else {
  218. this.prevHighlight = loci;
  219. }
  220. }
  221. return this.prevHighlight;
  222. }
  223. clearInteractiveHighlight() {
  224. const ret = this.prevHighlight;
  225. this.prevHighlight = void 0;
  226. return ret || EmptyLoci;
  227. }
  228. /** Count of all selected elements */
  229. elementCount() {
  230. let count = 0
  231. this.entries.forEach(v => {
  232. count += StructureElement.Loci.size(v.selection)
  233. })
  234. return count
  235. }
  236. getBoundary() {
  237. const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
  238. const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
  239. boundaryHelper.reset(0);
  240. const boundaries: Boundary[] = []
  241. this.entries.forEach(v => {
  242. const loci = v.selection
  243. if (!StructureElement.Loci.isEmpty(loci)) {
  244. boundaries.push(StructureElement.Loci.getBoundary(loci))
  245. }
  246. })
  247. for (let i = 0, il = boundaries.length; i < il; ++i) {
  248. const { box, sphere } = boundaries[i];
  249. Vec3.min(min, min, box.min);
  250. Vec3.max(max, max, box.max);
  251. boundaryHelper.boundaryStep(sphere.center, sphere.radius)
  252. }
  253. boundaryHelper.finishBoundaryStep();
  254. for (let i = 0, il = boundaries.length; i < il; ++i) {
  255. const { sphere } = boundaries[i];
  256. boundaryHelper.extendStep(sphere.center, sphere.radius);
  257. }
  258. return { box: { min, max }, sphere: boundaryHelper.getSphere() };
  259. }
  260. getPrincipalAxes(): PrincipalAxes {
  261. const elementCount = this.elementCount()
  262. const positions = new Float32Array(3 * elementCount)
  263. let offset = 0
  264. this.entries.forEach(v => {
  265. StructureElement.Loci.toPositionsArray(v.selection, positions, offset)
  266. offset += StructureElement.Loci.size(v.selection) * 3
  267. })
  268. return PrincipalAxes.ofPositions(positions)
  269. }
  270. modify(modifier: StructureSelectionModifier, loci: Loci) {
  271. let changed = false;
  272. switch (modifier) {
  273. case 'add': changed = this.add(loci); break;
  274. case 'remove': changed = this.remove(loci); break;
  275. case 'set': changed = this.set(loci); break;
  276. }
  277. if (changed) {
  278. this.state.stats = void 0;
  279. this.events.changed.next();
  280. }
  281. }
  282. private get applicableStructures() {
  283. // TODO: use "current structures" once implemented
  284. return this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure)).map(s => s.obj!.data)
  285. }
  286. private triggerInteraction(modifier: StructureSelectionModifier, loci: Loci, applyGranularity = true) {
  287. switch (modifier) {
  288. case 'add':
  289. this.plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity)
  290. break
  291. case 'remove':
  292. this.plugin.managers.interactivity.lociSelects.deselect({ loci }, applyGranularity)
  293. break
  294. case 'set':
  295. this.plugin.managers.interactivity.lociSelects.selectOnly({ loci }, applyGranularity)
  296. break
  297. }
  298. }
  299. fromSelectionQuery(modifier: StructureSelectionModifier, selectionQuery: StructureSelectionQuery, applyGranularity = true) {
  300. this.plugin.runTask(Task.create('Structure Selection', async runtime => {
  301. // const loci: Loci[] = [];
  302. for (const s of this.applicableStructures) {
  303. const loci = await StructureSelectionQuery.getLoci(this.plugin, runtime, selectionQuery, s);
  304. this.triggerInteraction(modifier, loci, applyGranularity);
  305. }
  306. }))
  307. }
  308. constructor(private plugin: PluginContext) {
  309. super({ entries: new Map(), history: [], stats: SelectionStats() });
  310. plugin.state.dataState.events.object.removed.subscribe(e => this.onRemove(e.ref));
  311. plugin.state.dataState.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
  312. }
  313. }
  314. interface SelectionStats {
  315. structureCount: number,
  316. elementCount: number,
  317. label: string
  318. }
  319. function SelectionStats(): SelectionStats { return { structureCount: 0, elementCount: 0, label: 'Nothing Selected' } };
  320. class SelectionEntry {
  321. private _selection: StructureElement.Loci;
  322. private _structure?: Structure = void 0;
  323. get selection() { return this._selection; }
  324. set selection(value: StructureElement.Loci) {
  325. this._selection = value;
  326. this._structure = void 0
  327. }
  328. get structure(): Structure | undefined {
  329. if (this._structure) return this._structure;
  330. if (Loci.isEmpty(this._selection)) {
  331. this._structure = void 0;
  332. } else {
  333. this._structure = StructureElement.Loci.toStructure(this._selection);
  334. }
  335. return this._structure;
  336. }
  337. constructor(selection: StructureElement.Loci) {
  338. this._selection = selection;
  339. }
  340. }
  341. interface HistoryEntry {
  342. loci: StructureElement.Loci,
  343. label: string
  344. }
  345. /** remap `selection-entry` to be related to `structure` if possible */
  346. function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry {
  347. return new SelectionEntry(StructureElement.Loci.remap(e.selection, s));
  348. }
  349. /**
  350. * Assumes `ref` and `ext` belong to the same unit in the same structure
  351. */
  352. function getElementRange(structure: Structure, ref: StructureElement.Loci['elements'][0], ext: StructureElement.Loci['elements'][0]) {
  353. const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices))
  354. const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices))
  355. return StructureElement.Loci(structure, [{
  356. unit: ref.unit,
  357. indices: OrderedSet.ofRange(min as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
  358. }]);
  359. }