From 4b323a670fccbb352cbb595436c69602e91913f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20Mal=C3=BD?= <michal.maly@ibt.cas.cz>
Date: Tue, 4 Aug 2020 11:23:28 +0200
Subject: [PATCH] Allow interactive selection of two consecutive residues

---
 src/mol-model/loci.ts                             |  9 +++++++--
 src/mol-model/structure/structure/element/loci.ts | 12 ++++++++----
 src/mol-plugin-ui/structure/focus.tsx             | 15 +++++++++++----
 .../selection/structure-focus-representation.ts   |  2 +-
 .../behavior/dynamic/volume-streaming/behavior.ts |  2 +-
 src/mol-theme/label.ts                            |  2 +-
 6 files changed, 29 insertions(+), 13 deletions(-)

diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index 5e2b27773..f6b55d333 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -222,7 +222,12 @@ namespace Loci {
         'element': (loci: Loci) => loci,
         'residue': (loci: Loci) => {
             return StructureElement.Loci.is(loci)
-                ? StructureElement.Loci.extendToWholeResidues(loci, true)
+                ? StructureElement.Loci.extendToWholeResidues(loci, 1, true)
+                : loci;
+        },
+        'two-residues': (loci: Loci) => {
+            return StructureElement.Loci.is(loci)
+                ? StructureElement.Loci.extendToWholeResidues(loci, 2, true)
                 : loci;
         },
         'chain': (loci: Loci) => {
@@ -261,7 +266,7 @@ namespace Loci {
         },
         'residueInstances': (loci: Loci) => {
             return StructureElement.Loci.is(loci)
-                ? StructureElement.Loci.extendToAllInstances(StructureElement.Loci.extendToWholeResidues(loci, true))
+                ? StructureElement.Loci.extendToAllInstances(StructureElement.Loci.extendToWholeResidues(loci, 1, true))
                 : loci;
         },
         'chainInstances': (loci: Loci) => {
diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts
index 9fefd8d8f..455d0a3bd 100644
--- a/src/mol-model/structure/structure/element/loci.ts
+++ b/src/mol-model/structure/structure/element/loci.ts
@@ -124,7 +124,7 @@ export namespace Loci {
 
     export function firstResidue(loci: Loci): Loci {
         if (isEmpty(loci)) return loci;
-        return extendToWholeResidues(firstElement(loci));
+        return extendToWholeResidues(firstElement(loci), 1);
     }
 
     export function firstChain(loci: Loci): Loci {
@@ -281,7 +281,7 @@ export namespace Loci {
         }
     }
 
-    export function extendToWholeResidues(loci: Loci, restrictToConformation?: boolean): Loci {
+    export function extendToWholeResidues(loci: Loci, count: number, restrictToConformation?: boolean): Loci {
         const elements: Loci['elements'][0][] = [];
         const residueAltIds = new Set<string>();
 
@@ -305,16 +305,20 @@ export namespace Loci {
                     residueAltIds.clear();
                     const eI = unitElements[OrderedSet.getAt(indices, i)];
                     const rI = residueIndex[eI];
+                    const rIEnd = rI + count >= residueIndex.length ? residueIndex.length - 1 : rI + count;
                     residueAltIds.add(label_alt_id.value(eI));
                     i++;
                     while (i < len) {
                         const eI = unitElements[OrderedSet.getAt(indices, i)];
-                        if (residueIndex[eI] !== rI) break;
+                        const _rI = residueIndex[eI];
+                        if (_rI < rI || _rI > rIEnd) {
+                            break;
+                        }
                         residueAltIds.add(label_alt_id.value(eI));
                         i++;
                     }
                     const hasSharedAltId = residueAltIds.has('');
-                    for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
+                    for (let j = residueOffsets[rI], _j = residueOffsets[rIEnd]; j < _j; j++) {
                         const idx = OrderedSet.indexOf(unitElements, j);
                         if (idx >= 0) {
                             const altId = label_alt_id.value(j);
diff --git a/src/mol-plugin-ui/structure/focus.tsx b/src/mol-plugin-ui/structure/focus.tsx
index f64af65c6..db4c8b7f4 100644
--- a/src/mol-plugin-ui/structure/focus.tsx
+++ b/src/mol-plugin-ui/structure/focus.tsx
@@ -24,14 +24,21 @@ interface StructureFocusControlsState {
     showAction: boolean
 }
 
-function addSymmetryGroupEntries(entries: Map<string, FocusEntry[]>, location: StructureElement.Location, unitSymmetryGroup: Unit.SymmetryGroup, granularity: 'residue' | 'chain') {
+function addSymmetryGroupEntries(entries: Map<string, FocusEntry[]>, location: StructureElement.Location, unitSymmetryGroup: Unit.SymmetryGroup, granularity: 'residue' | 'two-residues' | 'chain') {
     const idx = SortedArray.indexOf(location.unit.elements, location.element) as UnitIndex;
     const base = StructureElement.Loci(location.structure, [
         { unit: location.unit, indices: OrderedSet.ofSingleton(idx) }
     ]);
-    const extended = granularity === 'residue'
-        ? StructureElement.Loci.extendToWholeResidues(base)
-        : StructureElement.Loci.extendToWholeChains(base);
+    const extended = (() => {
+        switch (granularity) {
+            case 'residue':
+                return StructureElement.Loci.extendToWholeResidues(base, 1);
+            case 'two-residues':
+                return StructureElement.Loci.extendToWholeResidues(base, 2);
+            default:
+                return StructureElement.Loci.extendToWholeChains(base);
+        }
+    })();
     const name = StructureProperties.entity.pdbx_description(location).join(', ');
 
     for (const u of unitSymmetryGroup.units) {
diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts b/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts
index 344b5b1bb..eda773be8 100644
--- a/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts
+++ b/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts
@@ -160,7 +160,7 @@ class StructureFocusRepresentationBehavior extends PluginBehavior.WithSubscriber
         this.currentSource = sourceLoci;
         const loci = StructureElement.Loci.remap(sourceLoci, parent.obj!.data);
 
-        const residueLoci = StructureElement.Loci.extendToWholeResidues(loci);
+        const residueLoci = StructureElement.Loci.extendToWholeResidues(loci, 1);
         const residueBundle = StructureElement.Bundle.fromLoci(residueLoci);
 
         const target = StructureElement.Bundle.toExpression(residueBundle);
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
index 1050783fa..deaff2473 100644
--- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
@@ -340,7 +340,7 @@ export namespace VolumeStreaming {
             const transform = GlobalModelTransformInfo.get(root.obj?.data.models[0]!);
             if (transform) Mat4.invert(this._invTransform, transform);
 
-            const extendedLoci = StructureElement.Loci.extendToWholeResidues(loci);
+            const extendedLoci = StructureElement.Loci.extendToWholeResidues(loci, 1);
             const box = StructureElement.Loci.getBoundary(extendedLoci, transform && !Number.isNaN(this._invTransform[0]) ? this._invTransform : void 0).box;
 
             if (StructureElement.Loci.size(extendedLoci) === 1) {
diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts
index f04c8f658..1eae651ec 100644
--- a/src/mol-theme/label.ts
+++ b/src/mol-theme/label.ts
@@ -14,7 +14,7 @@ import { Vec3 } from '../mol-math/linear-algebra';
 import { radToDeg } from '../mol-math/misc';
 import { Volume } from '../mol-model/volume';
 
-export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure'
+export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'two-residues' | 'chain' | 'structure'
 
 export const DefaultLabelOptions = {
     granularity: 'element' as LabelGranularity,
-- 
GitLab