selection.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. /**
  2. * Copyright (c) 2019-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 { OrderedSet } from '../../../mol-data/int';
  8. import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
  9. import { Vec3 } from '../../../mol-math/linear-algebra';
  10. import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
  11. import { EmptyLoci, Loci } from '../../../mol-model/loci';
  12. import { QueryContext, Structure, StructureElement, StructureQuery, StructureSelection } from '../../../mol-model/structure';
  13. import { PluginContext } from '../../../mol-plugin/context';
  14. import { StateObjectRef, StateSelection } from '../../../mol-state';
  15. import { Task } from '../../../mol-task';
  16. import { structureElementStatsLabel } from '../../../mol-theme/label';
  17. import { arrayRemoveAtInPlace } from '../../../mol-util/array';
  18. import { StatefulPluginComponent } from '../../component';
  19. import { StructureSelectionQuery } from '../../helpers/structure-selection-query';
  20. import { PluginStateObject as PSO } from '../../objects';
  21. import { UUID } from '../../../mol-util';
  22. import { StructureRef } from './hierarchy-state';
  23. import { Boundary } from '../../../mol-math/geometry/boundary';
  24. import { iterableToArray } from '../../../mol-data/util';
  25. interface StructureSelectionManagerState {
  26. entries: Map<string, SelectionEntry>,
  27. additionsHistory: StructureSelectionHistoryEntry[],
  28. stats?: SelectionStats
  29. }
  30. const boundaryHelper = new BoundaryHelper('98');
  31. const HISTORY_CAPACITY = 24;
  32. export type StructureSelectionModifier = 'add' | 'remove' | 'intersect' | 'set'
  33. export type StructureSelectionSnapshot = {
  34. entries: {
  35. ref: string
  36. bundle: StructureElement.Bundle
  37. }[]
  38. }
  39. export class StructureSelectionManager extends StatefulPluginComponent<StructureSelectionManagerState> {
  40. readonly events = {
  41. changed: this.ev<undefined>(),
  42. additionsHistoryUpdated: this.ev<undefined>(),
  43. loci: {
  44. add: this.ev<StructureElement.Loci>(),
  45. remove: this.ev<StructureElement.Loci>(),
  46. clear: this.ev<undefined>()
  47. }
  48. };
  49. private referenceLoci: StructureElement.Loci | undefined;
  50. get entries() { return this.state.entries; }
  51. get additionsHistory() { return this.state.additionsHistory; }
  52. get stats() {
  53. if (this.state.stats) return this.state.stats;
  54. this.state.stats = this.calcStats();
  55. return this.state.stats;
  56. }
  57. private getEntry(s: Structure) {
  58. // ignore decorators to get stable ref
  59. const cell = this.plugin.helpers.substructureParent.get(s, true);
  60. if (!cell) return;
  61. const ref = cell.transform.ref;
  62. if (!this.entries.has(ref)) {
  63. const entry = new SelectionEntry(StructureElement.Loci(s, []));
  64. this.entries.set(ref, entry);
  65. return entry;
  66. }
  67. return this.entries.get(ref)!;
  68. }
  69. private calcStats(): SelectionStats {
  70. let structureCount = 0;
  71. let elementCount = 0;
  72. const stats = StructureElement.Stats.create();
  73. this.entries.forEach(v => {
  74. const { elements } = v.selection;
  75. if (elements.length) {
  76. structureCount += 1;
  77. for (let i = 0, il = elements.length; i < il; ++i) {
  78. elementCount += OrderedSet.size(elements[i].indices);
  79. }
  80. StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(v.selection));
  81. }
  82. });
  83. const label = structureElementStatsLabel(stats, { countsOnly: true });
  84. return { structureCount, elementCount, label };
  85. }
  86. private add(loci: Loci): boolean {
  87. if (!StructureElement.Loci.is(loci)) return false;
  88. const entry = this.getEntry(loci.structure);
  89. if (!entry) return false;
  90. const sel = entry.selection;
  91. entry.selection = StructureElement.Loci.union(entry.selection, loci);
  92. this.tryAddHistory(loci);
  93. this.referenceLoci = loci;
  94. this.events.loci.add.next(loci);
  95. return !StructureElement.Loci.areEqual(sel, entry.selection);
  96. }
  97. private remove(loci: Loci) {
  98. if (!StructureElement.Loci.is(loci)) return false;
  99. const entry = this.getEntry(loci.structure);
  100. if (!entry) return false;
  101. const sel = entry.selection;
  102. entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
  103. // this.addHistory(loci);
  104. this.referenceLoci = loci;
  105. this.events.loci.remove.next(loci);
  106. return !StructureElement.Loci.areEqual(sel, entry.selection);
  107. }
  108. private intersect(loci: Loci): boolean {
  109. if (!StructureElement.Loci.is(loci)) return false;
  110. const entry = this.getEntry(loci.structure);
  111. if (!entry) return false;
  112. const sel = entry.selection;
  113. entry.selection = StructureElement.Loci.intersect(entry.selection, loci);
  114. // this.addHistory(loci);
  115. this.referenceLoci = loci;
  116. return !StructureElement.Loci.areEqual(sel, entry.selection);
  117. }
  118. private set(loci: Loci) {
  119. if (!StructureElement.Loci.is(loci)) return false;
  120. const entry = this.getEntry(loci.structure);
  121. if (!entry) return false;
  122. const sel = entry.selection;
  123. entry.selection = loci;
  124. this.tryAddHistory(loci);
  125. this.referenceLoci = undefined;
  126. return !StructureElement.Loci.areEqual(sel, entry.selection);
  127. }
  128. modifyHistory(entry: StructureSelectionHistoryEntry, action: 'remove' | 'up' | 'down', modulus?: number, groupByStructure = false) {
  129. const history = this.additionsHistory;
  130. const idx = history.indexOf(entry);
  131. if (idx < 0) return;
  132. let swapWith: number | undefined = void 0;
  133. switch (action) {
  134. case 'remove': arrayRemoveAtInPlace(history, idx); break;
  135. case 'up': swapWith = idx - 1; break;
  136. case 'down': swapWith = idx + 1; break;
  137. }
  138. if (swapWith !== void 0) {
  139. const mod = modulus ? Math.min(history.length, modulus) : history.length;
  140. while (true) {
  141. swapWith = swapWith % mod;
  142. if (swapWith < 0) swapWith += mod;
  143. if (!groupByStructure || history[idx].loci.structure === history[swapWith].loci.structure) {
  144. const t = history[idx];
  145. history[idx] = history[swapWith];
  146. history[swapWith] = t;
  147. break;
  148. } else {
  149. swapWith += action === 'up' ? -1 : +1;
  150. }
  151. }
  152. }
  153. this.events.additionsHistoryUpdated.next(void 0);
  154. }
  155. private tryAddHistory(loci: StructureElement.Loci) {
  156. if (Loci.isEmpty(loci)) return;
  157. let idx = 0, entry: StructureSelectionHistoryEntry | undefined = void 0;
  158. for (const l of this.additionsHistory) {
  159. if (Loci.areEqual(l.loci, loci)) {
  160. entry = l;
  161. break;
  162. }
  163. idx++;
  164. }
  165. if (entry) {
  166. // move to top
  167. arrayRemoveAtInPlace(this.additionsHistory, idx);
  168. this.additionsHistory.unshift(entry);
  169. this.events.additionsHistoryUpdated.next(void 0);
  170. return;
  171. }
  172. const stats = StructureElement.Stats.ofLoci(loci);
  173. const label = structureElementStatsLabel(stats, { reverse: true });
  174. this.additionsHistory.unshift({ id: UUID.create22(), loci, label });
  175. if (this.additionsHistory.length > HISTORY_CAPACITY) this.additionsHistory.pop();
  176. this.events.additionsHistoryUpdated.next(void 0);
  177. }
  178. private clearHistory() {
  179. if (this.state.additionsHistory.length !== 0) {
  180. this.state.additionsHistory = [];
  181. this.events.additionsHistoryUpdated.next(void 0);
  182. }
  183. }
  184. private clearHistoryForStructure(structure: Structure) {
  185. const historyEntryToRemove: StructureSelectionHistoryEntry[] = [];
  186. for (const e of this.state.additionsHistory) {
  187. if (e.loci.structure.root === structure.root) {
  188. historyEntryToRemove.push(e);
  189. }
  190. }
  191. for (const e of historyEntryToRemove) {
  192. this.modifyHistory(e, 'remove');
  193. }
  194. if (historyEntryToRemove.length !== 0) {
  195. this.events.additionsHistoryUpdated.next(void 0);
  196. }
  197. }
  198. private onRemove(ref: string, obj: PSO.Molecule.Structure | undefined) {
  199. if (this.entries.has(ref)) {
  200. this.entries.delete(ref);
  201. if (obj?.data) {
  202. this.clearHistoryForStructure(obj.data);
  203. }
  204. if (this.referenceLoci?.structure === obj?.data) {
  205. this.referenceLoci = undefined;
  206. }
  207. this.state.stats = void 0;
  208. this.events.changed.next(void 0);
  209. }
  210. }
  211. private onUpdate(ref: string, oldObj: PSO.Molecule.Structure | undefined, obj: PSO.Molecule.Structure) {
  212. // no change to structure
  213. if (oldObj === obj || oldObj?.data === obj.data) return;
  214. // ignore decorators to get stable ref
  215. const cell = this.plugin.helpers.substructureParent.get(obj.data, true);
  216. if (!cell) return;
  217. // only need to update the root
  218. if (ref !== cell.transform.ref) return;
  219. if (!this.entries.has(ref)) return;
  220. // use structure from last decorator as reference
  221. const structure = this.plugin.helpers.substructureParent.get(obj.data)?.obj?.data;
  222. if (!structure) return;
  223. // oldObj is not defined for inserts (e.g. TransformStructureConformation)
  224. if (!oldObj?.data || Structure.areUnitIdsAndIndicesEqual(oldObj.data, obj.data)) {
  225. this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, structure));
  226. // remap referenceLoci & prevHighlight if needed and possible
  227. if (this.referenceLoci?.structure.root === structure.root) {
  228. this.referenceLoci = StructureElement.Loci.remap(this.referenceLoci, structure);
  229. }
  230. // remap history locis if needed and possible
  231. let changedHistory = false;
  232. for (const e of this.state.additionsHistory) {
  233. if (e.loci.structure.root === structure.root) {
  234. e.loci = StructureElement.Loci.remap(e.loci, structure);
  235. changedHistory = true;
  236. }
  237. }
  238. if (changedHistory) this.events.additionsHistoryUpdated.next(void 0);
  239. } else {
  240. // clear the selection for ref
  241. this.entries.set(ref, new SelectionEntry(StructureElement.Loci(structure, [])));
  242. if (this.referenceLoci?.structure.root === structure.root) {
  243. this.referenceLoci = undefined;
  244. }
  245. this.clearHistoryForStructure(structure);
  246. this.state.stats = void 0;
  247. this.events.changed.next(void 0);
  248. }
  249. }
  250. /** Removes all selections and returns them */
  251. clear() {
  252. const keys = this.entries.keys();
  253. const selections: StructureElement.Loci[] = [];
  254. while (true) {
  255. const k = keys.next();
  256. if (k.done) break;
  257. const s = this.entries.get(k.value)!;
  258. if (!StructureElement.Loci.isEmpty(s.selection)) selections.push(s.selection);
  259. s.selection = StructureElement.Loci(s.selection.structure, []);
  260. }
  261. this.referenceLoci = undefined;
  262. this.state.stats = void 0;
  263. this.events.changed.next(void 0);
  264. this.events.loci.clear.next(void 0);
  265. this.clearHistory();
  266. return selections;
  267. }
  268. getLoci(structure: Structure) {
  269. const entry = this.getEntry(structure);
  270. if (!entry) return EmptyLoci;
  271. return entry.selection;
  272. }
  273. getStructure(structure: Structure) {
  274. const entry = this.getEntry(structure);
  275. if (!entry) return;
  276. return entry.structure;
  277. }
  278. structureHasSelection(structure: StructureRef) {
  279. const s = structure.cell?.obj?.data;
  280. if (!s) return false;
  281. const entry = this.getEntry(s);
  282. return !!entry && !StructureElement.Loci.isEmpty(entry.selection);
  283. }
  284. has(loci: Loci) {
  285. if (StructureElement.Loci.is(loci)) {
  286. const entry = this.getEntry(loci.structure);
  287. if (entry) {
  288. return StructureElement.Loci.isSubset(entry.selection, loci);
  289. }
  290. }
  291. return false;
  292. }
  293. tryGetRange(loci: Loci): StructureElement.Loci | undefined {
  294. if (!StructureElement.Loci.is(loci)) return;
  295. if (loci.elements.length !== 1) return;
  296. const entry = this.getEntry(loci.structure);
  297. if (!entry) return;
  298. const xs = loci.elements[0];
  299. if (!xs) return;
  300. const ref = this.referenceLoci;
  301. if (!ref || !StructureElement.Loci.is(ref) || ref.structure !== loci.structure) return;
  302. let e: StructureElement.Loci['elements'][0] | undefined;
  303. for (const _e of ref.elements) {
  304. if (xs.unit === _e.unit) {
  305. e = _e;
  306. break;
  307. }
  308. }
  309. if (!e) return;
  310. if (xs.unit !== e.unit) return;
  311. return getElementRange(loci.structure, e, xs);
  312. }
  313. /** Count of all selected elements */
  314. elementCount() {
  315. let count = 0;
  316. this.entries.forEach(v => {
  317. count += StructureElement.Loci.size(v.selection);
  318. });
  319. return count;
  320. }
  321. getBoundary() {
  322. const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
  323. const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
  324. boundaryHelper.reset();
  325. const boundaries: Boundary[] = [];
  326. this.entries.forEach(v => {
  327. const loci = v.selection;
  328. if (!StructureElement.Loci.isEmpty(loci)) {
  329. boundaries.push(StructureElement.Loci.getBoundary(loci));
  330. }
  331. });
  332. for (let i = 0, il = boundaries.length; i < il; ++i) {
  333. const { box, sphere } = boundaries[i];
  334. Vec3.min(min, min, box.min);
  335. Vec3.max(max, max, box.max);
  336. boundaryHelper.includePositionRadius(sphere.center, sphere.radius);
  337. }
  338. boundaryHelper.finishedIncludeStep();
  339. for (let i = 0, il = boundaries.length; i < il; ++i) {
  340. const { sphere } = boundaries[i];
  341. boundaryHelper.radiusPositionRadius(sphere.center, sphere.radius);
  342. }
  343. return { box: { min, max }, sphere: boundaryHelper.getSphere() };
  344. }
  345. getPrincipalAxes(): PrincipalAxes {
  346. const values = iterableToArray(this.entries.values());
  347. return StructureElement.Loci.getPrincipalAxesMany(values.map(v => v.selection));
  348. }
  349. modify(modifier: StructureSelectionModifier, loci: Loci) {
  350. let changed = false;
  351. switch (modifier) {
  352. case 'add': changed = this.add(loci); break;
  353. case 'remove': changed = this.remove(loci); break;
  354. case 'intersect': changed = this.intersect(loci); break;
  355. case 'set': changed = this.set(loci); break;
  356. }
  357. if (changed) {
  358. this.state.stats = void 0;
  359. this.events.changed.next(void 0);
  360. }
  361. }
  362. private get applicableStructures() {
  363. return this.plugin.managers.structure.hierarchy.selection.structures
  364. .filter(s => !!s.cell.obj)
  365. .map(s => s.cell.obj!.data);
  366. }
  367. private triggerInteraction(modifier: StructureSelectionModifier, loci: Loci, applyGranularity = true) {
  368. switch (modifier) {
  369. case 'add':
  370. this.plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
  371. break;
  372. case 'remove':
  373. this.plugin.managers.interactivity.lociSelects.deselect({ loci }, applyGranularity);
  374. break;
  375. case 'intersect':
  376. this.plugin.managers.interactivity.lociSelects.selectJoin({ loci }, applyGranularity);
  377. break;
  378. case 'set':
  379. this.plugin.managers.interactivity.lociSelects.selectOnly({ loci }, applyGranularity);
  380. break;
  381. }
  382. }
  383. fromLoci(modifier: StructureSelectionModifier, loci: Loci, applyGranularity = true) {
  384. this.triggerInteraction(modifier, loci, applyGranularity);
  385. }
  386. fromCompiledQuery(modifier: StructureSelectionModifier, query: StructureQuery, applyGranularity = true) {
  387. for (const s of this.applicableStructures) {
  388. const loci = query(new QueryContext(s));
  389. this.triggerInteraction(modifier, StructureSelection.toLociWithSourceUnits(loci), applyGranularity);
  390. }
  391. }
  392. fromSelectionQuery(modifier: StructureSelectionModifier, query: StructureSelectionQuery, applyGranularity = true) {
  393. this.plugin.runTask(Task.create('Structure Selection', async runtime => {
  394. for (const s of this.applicableStructures) {
  395. const loci = await query.getSelection(this.plugin, runtime, s);
  396. this.triggerInteraction(modifier, StructureSelection.toLociWithSourceUnits(loci), applyGranularity);
  397. }
  398. }));
  399. }
  400. fromSelections(ref: StateObjectRef<PSO.Molecule.Structure.Selections>) {
  401. const cell = StateObjectRef.resolveAndCheck(this.plugin.state.data, ref);
  402. if (!cell || !cell.obj) return;
  403. if (!PSO.Molecule.Structure.Selections.is(cell.obj)) {
  404. console.warn('fromSelections applied to wrong object type.', cell.obj);
  405. return;
  406. }
  407. this.clear();
  408. for (const s of cell.obj?.data) {
  409. this.fromLoci('set', s.loci);
  410. }
  411. }
  412. getSnapshot(): StructureSelectionSnapshot {
  413. const entries: StructureSelectionSnapshot['entries'] = [];
  414. this.entries.forEach((entry, ref) => {
  415. entries.push({
  416. ref,
  417. bundle: StructureElement.Bundle.fromLoci(entry.selection)
  418. });
  419. });
  420. return { entries };
  421. }
  422. setSnapshot(snapshot: StructureSelectionSnapshot) {
  423. this.entries.clear();
  424. for (const { ref, bundle } of snapshot.entries) {
  425. const structure = this.plugin.state.data.select(StateSelection.Generators.byRef(ref))[0]?.obj?.data as Structure;
  426. if (!structure) continue;
  427. const loci = StructureElement.Bundle.toLoci(bundle, structure);
  428. this.fromLoci('set', loci, false);
  429. }
  430. }
  431. constructor(private plugin: PluginContext) {
  432. super({ entries: new Map(), additionsHistory: [], stats: SelectionStats() });
  433. // listen to events from substructureParent helper to ensure it is updated
  434. plugin.helpers.substructureParent.events.removed.subscribe(e => this.onRemove(e.ref, e.obj));
  435. plugin.helpers.substructureParent.events.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
  436. }
  437. }
  438. interface SelectionStats {
  439. structureCount: number,
  440. elementCount: number,
  441. label: string
  442. }
  443. function SelectionStats(): SelectionStats { return { structureCount: 0, elementCount: 0, label: 'Nothing Selected' }; };
  444. class SelectionEntry {
  445. private _selection: StructureElement.Loci;
  446. private _structure?: Structure = void 0;
  447. get selection() { return this._selection; }
  448. set selection(value: StructureElement.Loci) {
  449. this._selection = value;
  450. this._structure = void 0;
  451. }
  452. get structure(): Structure | undefined {
  453. if (this._structure) return this._structure;
  454. if (Loci.isEmpty(this._selection)) {
  455. this._structure = void 0;
  456. } else {
  457. this._structure = StructureElement.Loci.toStructure(this._selection);
  458. }
  459. return this._structure;
  460. }
  461. constructor(selection: StructureElement.Loci) {
  462. this._selection = selection;
  463. }
  464. }
  465. export interface StructureSelectionHistoryEntry {
  466. id: UUID,
  467. loci: StructureElement.Loci,
  468. label: string
  469. }
  470. /** remap `selection-entry` to be related to `structure` if possible */
  471. function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry {
  472. return new SelectionEntry(StructureElement.Loci.remap(e.selection, s));
  473. }
  474. /**
  475. * Assumes `ref` and `ext` belong to the same unit in the same structure
  476. */
  477. function getElementRange(structure: Structure, ref: StructureElement.Loci['elements'][0], ext: StructureElement.Loci['elements'][0]) {
  478. const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices));
  479. const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices));
  480. return StructureElement.Loci(structure, [{
  481. unit: ref.unit,
  482. indices: OrderedSet.ofRange(min as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
  483. }]);
  484. }