From 38f77360a7ad51cf5c847269192bbf18efaa5633 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20Mal=C3=BD?= <michal.maly@ibt.cas.cz>
Date: Fri, 15 Apr 2022 17:49:04 +0200
Subject: [PATCH] ReDNATCO plugin stage 8

---
 src/apps/rednatco/color-picker.tsx     |   2 +
 src/apps/rednatco/controls.tsx         |  34 +-
 src/apps/rednatco/index.html           |   2 +-
 src/apps/rednatco/index.tsx            | 523 ++++++++++++++-----------
 src/apps/rednatco/rednatco-molstar.css |  24 +-
 src/apps/rednatco/util.ts              |  19 +-
 webpack.config.common.js               |  10 +
 7 files changed, 359 insertions(+), 255 deletions(-)

diff --git a/src/apps/rednatco/color-picker.tsx b/src/apps/rednatco/color-picker.tsx
index 3958ea3d9..d57b2037f 100644
--- a/src/apps/rednatco/color-picker.tsx
+++ b/src/apps/rednatco/color-picker.tsx
@@ -12,6 +12,8 @@ import ReactDOM from 'react-dom';
 import { Colors } from './colors';
 import { PushButton, SpinBox } from './controls';
 import { parseInt as parseIntMS } from '../../mol-io/reader/common/text/number-parser';
+import './assets/imgs/triangle-down.svg';
+import './assets/imgs/triangle-up.svg';
 
 const PALETTE_CURSOR_HALFSIZE = 10;
 const VALUE_CURSOR_THICKNESS = 3;
diff --git a/src/apps/rednatco/controls.tsx b/src/apps/rednatco/controls.tsx
index 62e751bb9..f72e9e58c 100644
--- a/src/apps/rednatco/controls.tsx
+++ b/src/apps/rednatco/controls.tsx
@@ -1,5 +1,34 @@
 import React from 'react';
 
+export class CollapsibleVertical extends React.Component<CollapsibleVertical.Props, { collapsed: boolean }> {
+    constructor(props: CollapsibleVertical.Props) {
+        super(props);
+
+        this.state = {
+            collapsed: true,
+        };
+    }
+
+    render() {
+        return (
+            <div className='rmsp-collapsible-vertical'>
+                <div
+                    className='rmsp-collapsible-vertical-caption'
+                    onClick={() => this.setState({ ...this.state, collapsed: !this.state.collapsed })}
+                >
+                    {this.props.caption}
+                </div>
+                {this.state.collapsed ? undefined : this.props.children}
+            </div>
+        );
+    }
+}
+export namespace CollapsibleVertical {
+    export interface Props {
+        caption: string;
+    }
+}
+
 export class PushButton extends React.Component<{ text: string, enabled: boolean, onClick: () => void }> {
     render() {
         return (
@@ -77,18 +106,17 @@ export class SpinBox extends React.Component<SpinBox.Props> {
                 <div className='rmsp-spinbox-buttons'>
                     <img
                         className='rmsp-spinbox-button'
-                        src={`./${this.props.pathPrefix}assets/imgs/triangle-up.svg`} onClick={() => this.increase()}
+                        src={`./${this.props.pathPrefix}imgs/triangle-up.svg`} onClick={() => this.increase()}
                     />
                     <img
                         className='rmsp-spinbox-button'
-                        src={`./${this.props.pathPrefix}assets/imgs/triangle-down.svg`} onClick={() => this.decrease()}
+                        src={`./${this.props.pathPrefix}imgs/triangle-down.svg`} onClick={() => this.decrease()}
                     />
                 </div>
             </div>
         );
     }
 }
-
 export namespace SpinBox {
     export interface Formatter {
         (v: number|null): string;
diff --git a/src/apps/rednatco/index.html b/src/apps/rednatco/index.html
index 3cf8bfba8..6b0daeb19 100644
--- a/src/apps/rednatco/index.html
+++ b/src/apps/rednatco/index.html
@@ -9,7 +9,7 @@
         <script type="text/javascript" src="./molstar.js"></script>
         <script>
             async function loadStructure() {
-                const resp = await fetch('./1hmh_v41C35A23.cif');
+                const resp = await fetch('./1dk1_v41C35A23.cif');
                 const data = await resp.text();
 
                 molstar.ReDNATCOMspApi.loadStructure(data);
diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx
index 2e91560a2..d4f395352 100644
--- a/src/apps/rednatco/index.tsx
+++ b/src/apps/rednatco/index.tsx
@@ -3,18 +3,20 @@ import ReactDOM from 'react-dom';
 import { NtCColors } from './colors';
 import { ColorPicker } from './color-picker';
 import { Commands } from './commands';
-import { PushButton, ToggleButton } from './controls';
+import { CollapsibleVertical, PushButton, ToggleButton } from './controls';
 import * as IDs from './idents';
 import * as RefCfmr from './reference-conformers';
 import { ReferenceConformersPdbs } from './reference-conformers-pdbs';
 import { Step } from './step';
 import { Superpose } from './superpose';
 import { Traverse } from './traverse';
+import { luminance } from './util';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation';
 import { OrderedSet } from '../../mol-data/int/ordered-set';
 import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
-import { Loci } from '../../mol-model/loci';
+import { Vec3 } from '../../mol-math/linear-algebra/3d';
+import { EmptyLoci, Loci } from '../../mol-model/loci';
 import { ElementIndex, Model, Structure, StructureElement, StructureProperties, StructureSelection, Trajectory } from '../../mol-model/structure';
 import { Location } from '../../mol-model/structure/structure/element/location';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
@@ -54,9 +56,55 @@ const RCRef = 'rc';
 const NtCSupPrev = 'ntc-sup-prev';
 const NtCSupSel = 'ntc-sup-sel';
 const NtCSupNext = 'ntc-sup-next';
-
 const SphereBoundaryHelper = new BoundaryHelper('98');
 
+const ConformersByClass = {
+    A: ['AA00_Upr', 'AA00_Lwr', 'AA02_Upr', 'AA02_Lwr', 'AA03_Upr', 'AA03_Lwr', 'AA04_Upr', 'AA04_Lwr', 'AA08_Upr', 'AA08_Lwr', 'AA09_Upr', 'AA09_Lwr', 'AA01_Upr', 'AA01_Lwr', 'AA05_Upr', 'AA05_Lwr', 'AA06_Upr', 'AA06_Lwr', 'AA10_Upr', 'AA10_Lwr', 'AA11_Upr', 'AA11_Lwr', 'AA07_Upr', 'AA07_Lwr', 'AA12_Upr', 'AA12_Lwr', 'AA13_Upr', 'AA13_Lwr', 'AB01_Upr', 'AB02_Upr', 'AB03_Upr', 'AB04_Upr', 'AB05_Upr', 'BA01_Lwr', 'BA05_Lwr', 'BA09_Lwr', 'BA08_Lwr', 'BA10_Lwr', 'BA13_Lwr', 'BA16_Lwr', 'BA17_Lwr', 'AAS1_Lwr', 'AB1S_Upr'],
+    B: ['AB01_Lwr', 'AB02_Lwr', 'AB03_Lwr', 'AB04_Lwr', 'AB05_Lwr', 'BA09_Upr', 'BA10_Upr', 'BB00_Upr', 'BB00_Lwr', 'BB01_Upr', 'BB01_Lwr', 'BB17_Upr', 'BB17_Lwr', 'BB02_Upr', 'BB02_Lwr', 'BB03_Upr', 'BB03_Lwr', 'BB11_Upr', 'BB11_Lwr', 'BB16_Upr', 'BB16_Lwr', 'BB04_Upr', 'BB05_Upr', 'BB1S_Upr', 'BB2S_Upr', 'BBS1_Lwr'],
+    BII: ['BA08_Upr', 'BA13_Upr', 'BA16_Upr', 'BA17_Upr', 'BB04_Lwr', 'BB05_Lwr', 'BB07_Upr', 'BB07_Lwr', 'BB08_Upr', 'BB08_Lwr'],
+    miB: ['BB10_Upr', 'BB10_Lwr', 'BB12_Upr', 'BB12_Lwr', 'BB13_Upr', 'BB13_Lwr', 'BB14_Upr', 'BB14_Lwr', 'BB15_Upr', 'BB15_Lwr', 'BB20_Upr', 'BB20_Lwr'],
+    IC: ['IC01_Upr', 'IC01_Lwr', 'IC02_Upr', 'IC02_Lwr', 'IC03_Upr', 'IC03_Lwr', 'IC04_Upr', 'IC04_Lwr', 'IC05_Upr', 'IC05_Lwr', 'IC06_Upr', 'IC06_Lwr', 'IC07_Upr', 'IC07_Lwr'],
+    OPN: ['OP01_Upr', 'OP01_Lwr', 'OP02_Upr', 'OP02_Lwr', 'OP03_Upr', 'OP03_Lwr', 'OP04_Upr', 'OP04_Lwr', 'OP05_Upr', 'OP05_Lwr', 'OP06_Upr', 'OP06_Lwr', 'OP07_Upr', 'OP07_Lwr', 'OP08_Upr', 'OP08_Lwr', 'OP09_Upr', 'OP09_Lwr', 'OP10_Upr', 'OP10_Lwr', 'OP11_Upr', 'OP11_Lwr', 'OP12_Upr', 'OP12_Lwr', 'OP13_Upr', 'OP13_Lwr', 'OP14_Upr', 'OP14_Lwr', 'OP15_Upr', 'OP15_Lwr', 'OP16_Upr', 'OP16_Lwr', 'OP17_Upr', 'OP17_Lwr', 'OP18_Upr', 'OP18_Lwr', 'OP19_Upr', 'OP19_Lwr', 'OP20_Upr', 'OP20_Lwr', 'OP21_Upr', 'OP21_Lwr', 'OP22_Upr', 'OP22_Lwr', 'OP23_Upr', 'OP23_Lwr', 'OP24_Upr', 'OP24_Lwr', 'OP25_Upr', 'OP25_Lwr', 'OP26_Upr', 'OP26_Lwr', 'OP27_Upr', 'OP27_Lwr', 'OP28_Upr', 'OP28_Lwr', 'OP29_Upr', 'OP29_Lwr', 'OP30_Upr', 'OP30_Lwr', 'OP31_Upr', 'OP31_Lwr', 'OPS1_Upr', 'OPS1_Lwr', 'OP1S_Upr', 'OP1S_Lwr'],
+    SYN: ['AAS1_Upr', 'AB1S_Lwr', 'AB2S_Lwr', 'BB1S_Lwr', 'BB2S_Lwr', 'BBS1_Upr', 'ZZ1S_Lwr', 'ZZ2S_Lwr', 'ZZS1_Upr', 'ZZS2_Upr'],
+    Z: ['ZZ01_Upr', 'ZZ01_Lwr', 'ZZ02_Upr', 'ZZ02_Lwr', 'ZZ1S_Upr', 'ZZ2S_Upr', 'ZZS1_Lwr', 'ZZS2_Lwr'],
+    N: ['NANT_Upr', 'NANT_Lwr'],
+};
+type ConformersByClass = typeof ConformersByClass;
+
+class ColorBox extends React.Component<{ caption: string, color: Color }> {
+    render() {
+        const lum = luminance(this.props.color);
+        return (
+            <div
+                className='rmsp-color-box'
+                style={{ backgroundColor: Color.toStyle(this.props.color) }}
+            >
+                <span style={{ color: lum > 0.6 ? 'black' : 'white' }}>{this.props.caption}</span>
+            </div>
+        );
+    }
+}
+
+const Display = {
+    representation: 'cartoon' as VisualRepresentations,
+
+    showNucleic: true,
+    showProtein: false,
+    showWater: false,
+
+    showPyramids: true,
+    pyramidsTransparent: false,
+
+    showBalls: false,
+    ballsTransparent: false,
+
+    modelNumber: 1,
+
+    classColors: { ...NtCColors.Classes },
+    conformerColors: { ...NtCColors.Conformers },
+};
+type Display = typeof Display;
+
 type StepInfo = {
     name: string;
     assignedNtC: string;
@@ -67,11 +115,12 @@ type StepInfo = {
     altId2?: string;
 }
 
+type VisualRepresentations = 'ball-and-stick'|'cartoon';
+
 function capitalize(s: string) {
     if (s.length === 0)
         return s;
     return s[0].toLocaleUpperCase() + s.slice(1);
-
 }
 
 function dinucleotideBackbone(loci: StructureElement.Loci, altId1?: string, altId2?: string) {
@@ -146,54 +195,6 @@ function rcref(c: string, where: 'sel'|'prev'|'next'|'' = '') {
     return `${RCRef}-${c}-${where}`;
 }
 
-class ColorBox extends React.Component<{ caption: string, color: Color }> {
-    render() {
-        return (
-            <div className='rmsp-color-box'>
-                <div className='rmsp-color-box-caption'>{this.props.caption}</div>
-                <div
-                    className='rmsp-color-box-color'
-                    style={{ backgroundColor: Color.toStyle(this.props.color) }}
-                />
-            </div>
-        );
-    }
-}
-
-const ConformersByClass = {
-    A: ['AA00_Upr', 'AA00_Lwr', 'AA02_Upr', 'AA02_Lwr', 'AA03_Upr', 'AA03_Lwr', 'AA04_Upr', 'AA04_Lwr', 'AA08_Upr', 'AA08_Lwr', 'AA09_Upr', 'AA09_Lwr', 'AA01_Upr', 'AA01_Lwr', 'AA05_Upr', 'AA05_Lwr', 'AA06_Upr', 'AA06_Lwr', 'AA10_Upr', 'AA10_Lwr', 'AA11_Upr', 'AA11_Lwr', 'AA07_Upr', 'AA07_Lwr', 'AA12_Upr', 'AA12_Lwr', 'AA13_Upr', 'AA13_Lwr', 'AB01_Upr', 'AB02_Upr', 'AB03_Upr', 'AB04_Upr', 'AB05_Upr', 'BA01_Lwr', 'BA05_Lwr', 'BA09_Lwr', 'BA08_Lwr', 'BA10_Lwr', 'BA13_Lwr', 'BA16_Lwr', 'BA17_Lwr', 'AAS1_Lwr', 'AB1S_Upr'],
-    B: ['AB01_Lwr', 'AB02_Lwr', 'AB03_Lwr', 'AB04_Lwr', 'AB05_Lwr', 'BA09_Upr', 'BA10_Upr', 'BB00_Upr', 'BB00_Lwr', 'BB01_Upr', 'BB01_Lwr', 'BB17_Upr', 'BB17_Lwr', 'BB02_Upr', 'BB02_Lwr', 'BB03_Upr', 'BB03_Lwr', 'BB11_Upr', 'BB11_Lwr', 'BB16_Upr', 'BB16_Lwr', 'BB04_Upr', 'BB05_Upr', 'BB1S_Upr', 'BB2S_Upr', 'BBS1_Lwr'],
-    BII: ['BA08_Upr', 'BA13_Upr', 'BA16_Upr', 'BA17_Upr', 'BB04_Lwr', 'BB05_Lwr', 'BB07_Upr', 'BB07_Lwr', 'BB08_Upr', 'BB08_Lwr'],
-    miB: ['BB10_Upr', 'BB10_Lwr', 'BB12_Upr', 'BB12_Lwr', 'BB13_Upr', 'BB13_Lwr', 'BB14_Upr', 'BB14_Lwr', 'BB15_Upr', 'BB15_Lwr', 'BB20_Upr', 'BB20_Lwr'],
-    IC: ['IC01_Upr', 'IC01_Lwr', 'IC02_Upr', 'IC02_Lwr', 'IC03_Upr', 'IC03_Lwr', 'IC04_Upr', 'IC04_Lwr', 'IC05_Upr', 'IC05_Lwr', 'IC06_Upr', 'IC06_Lwr', 'IC07_Upr', 'IC07_Lwr'],
-    OPN: ['OP01_Upr', 'OP01_Lwr', 'OP02_Upr', 'OP02_Lwr', 'OP03_Upr', 'OP03_Lwr', 'OP04_Upr', 'OP04_Lwr', 'OP05_Upr', 'OP05_Lwr', 'OP06_Upr', 'OP06_Lwr', 'OP07_Upr', 'OP07_Lwr', 'OP08_Upr', 'OP08_Lwr', 'OP09_Upr', 'OP09_Lwr', 'OP10_Upr', 'OP10_Lwr', 'OP11_Upr', 'OP11_Lwr', 'OP12_Upr', 'OP12_Lwr', 'OP13_Upr', 'OP13_Lwr', 'OP14_Upr', 'OP14_Lwr', 'OP15_Upr', 'OP15_Lwr', 'OP16_Upr', 'OP16_Lwr', 'OP17_Upr', 'OP17_Lwr', 'OP18_Upr', 'OP18_Lwr', 'OP19_Upr', 'OP19_Lwr', 'OP20_Upr', 'OP20_Lwr', 'OP21_Upr', 'OP21_Lwr', 'OP22_Upr', 'OP22_Lwr', 'OP23_Upr', 'OP23_Lwr', 'OP24_Upr', 'OP24_Lwr', 'OP25_Upr', 'OP25_Lwr', 'OP26_Upr', 'OP26_Lwr', 'OP27_Upr', 'OP27_Lwr', 'OP28_Upr', 'OP28_Lwr', 'OP29_Upr', 'OP29_Lwr', 'OP30_Upr', 'OP30_Lwr', 'OP31_Upr', 'OP31_Lwr', 'OPS1_Upr', 'OPS1_Lwr', 'OP1S_Upr', 'OP1S_Lwr'],
-    SYN: ['AAS1_Upr', 'AB1S_Lwr', 'AB2S_Lwr', 'BB1S_Lwr', 'BB2S_Lwr', 'BBS1_Upr', 'ZZ1S_Lwr', 'ZZ2S_Lwr', 'ZZS1_Upr', 'ZZS2_Upr'],
-    Z: ['ZZ01_Upr', 'ZZ01_Lwr', 'ZZ02_Upr', 'ZZ02_Lwr', 'ZZ1S_Upr', 'ZZ2S_Upr', 'ZZS1_Lwr', 'ZZS2_Lwr'],
-    N: ['NANT_Upr', 'NANT_Lwr'],
-};
-type ConformersByClass = typeof ConformersByClass;
-
-type VisualRepresentations = 'ball-and-stick'|'cartoon';
-const Display = {
-    representation: 'cartoon' as VisualRepresentations,
-
-    showNucleic: true,
-    showProtein: false,
-    showWater: false,
-
-    showPyramids: true,
-    pyramidsTransparent: false,
-
-    showBalls: false,
-    ballsTransparent: false,
-
-    modelNumber: 1,
-
-    classColors: { ...NtCColors.Classes },
-    conformerColors: { ...NtCColors.Conformers },
-};
-type Display = typeof Display;
-
 const ReDNATCOLociLabelProvider = PluginBehavior.create({
     name: 'watlas-loci-label-provider',
     category: 'interaction',
@@ -360,6 +361,37 @@ class ReDNATCOMspViewer {
         return (model as StateObject<Model>).data.modelNum;
     }
 
+    private focusOnLoci(loci: StructureElement.Loci) {
+        if (!this.plugin.canvas3d)
+            return;
+
+        const sphere = Loci.getBoundingSphere(loci);
+        if (!sphere)
+            return;
+        const snapshot = this.plugin.canvas3d.camera.getSnapshot();
+
+        const v = Vec3();
+        const u = Vec3();
+        Vec3.set(v, sphere.center[0], sphere.center[1], sphere.center[2]);
+        Vec3.set(u, snapshot.position[0], snapshot.position[1], snapshot.position[2]);
+        Vec3.sub(u, u, v);
+        Vec3.normalize(u, u);
+        Vec3.scale(u, u, sphere.radius * 8);
+        Vec3.add(v, u, v);
+
+        console.log(
+            'Cam',
+            'Center', sphere.center,
+            'Radius', sphere.radius,
+            'Position', v
+        );
+
+        snapshot.target = sphere.center;
+        snapshot.position = v;
+
+        PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: AnimationDurationMsec });
+    }
+
     private getBuilder(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
         return this.plugin.state.data.build().to(IDs.ID(id, sub, ref));
     }
@@ -588,7 +620,7 @@ class ReDNATCOMspViewer {
         const b = this.plugin.state.data.build();
         const repr = display.representation ?? 'cartoon';
 
-        for (const sub of ['nucleic', 'protein', 'water'] as IDs.Substructure[]) {
+        for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) {
             if (this.has('visual', sub)) {
                 b.to(IDs.ID('visual', sub, BaseRef))
                     .update(
@@ -604,6 +636,26 @@ class ReDNATCOMspViewer {
         await b.commit();
     }
 
+    focusOnSelectedStep() {
+        // Focus camera on the selection
+        const sel = this.plugin.state.data.cells.get(IDs.ID('superposition', '', NtCSupSel));
+        const prev = this.plugin.state.data.cells.get(IDs.ID('superposition', '', NtCSupPrev));
+        const next = this.plugin.state.data.cells.get(IDs.ID('superposition', '', NtCSupNext));
+
+        const prevLoci = prev?.obj ? StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(prev.obj!.data, prev.obj!.data)) : EmptyLoci;
+        const nextLoci = next?.obj ? StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(next.obj!.data, next.obj!.data)) : EmptyLoci;
+        let focusOn = sel?.obj ? StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(sel!.obj!.data, sel!.obj!.data)) : EmptyLoci;
+        if (focusOn.kind === 'empty-loci')
+            return;
+
+        if (prevLoci.kind === 'element-loci')
+            focusOn = StructureElement.Loci.union(focusOn, prevLoci);
+        if (nextLoci.kind === 'element-loci')
+            focusOn = StructureElement.Loci.union(focusOn, nextLoci);
+
+        this.focusOnLoci(focusOn);
+    }
+
     gatherStepInfo(): { steps: StepInfo[], stepNames: Map<string, number> }|undefined {
         const obj = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
         if (!obj)
@@ -724,7 +776,7 @@ class ReDNATCOMspViewer {
     }
 
     has(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
-        return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj;
+        return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj?.data;
     }
 
     isReady() {
@@ -819,15 +871,17 @@ class ReDNATCOMspViewer {
         await b.commit();
     }
 
-    onDeselected() {
-        this.plugin.state.data.build()
+    async onDeselected() {
+        await this.plugin.state.data.build()
             .delete(IDs.ID('superposition', '', NtCSupSel))
             .delete(IDs.ID('superposition', '', NtCSupPrev))
             .delete(IDs.ID('superposition', '', NtCSupNext))
             .commit();
+
+        this.resetCameraRadius();
     }
 
-    onLociSelected(selected: Representation.Loci) {
+    async onLociSelected(selected: Representation.Loci) {
         const loci = Loci.normalize(selected.loci, 'two-residues');
 
         if (loci.kind === 'element-loci') {
@@ -838,7 +892,8 @@ class ReDNATCOMspViewer {
             if (!stepDesc)
                 return;
             const stepName = Step.name(stepDesc, this.haveMultipleModels);
-            this.superposeReferences(stepName, '', []);
+            await this.superposeReferences(stepName, '', []);
+            this.focusOnSelectedStep();
         }
     }
 
@@ -957,7 +1012,7 @@ class ReDNATCOMspViewer {
             addReference(ntcRefNext, NtCSupNext, Loci.normalize(Traverse.residue(1, altId1, selLoci), 'two-residues'), altId1, altId2, 0x00FFFF);
         }
 
-        b.commit();
+        await b.commit();
 
         return rmsd;
     }
@@ -1000,14 +1055,20 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
         return updated;
     }
 
-    private updateClassColor(k: keyof NtCColors.Classes, color: number) {
-        const clr = Color(color);
+    private updateClassColor(changes: { cls: keyof NtCColors.Classes, color: number }|{ cls: keyof NtCColors.Classes, color: number }[]) {
         const classColors = { ...this.state.display.classColors };
-        classColors[k] = clr;
 
-        const conformerColors = {
+        const isArray = Array.isArray(changes);
+        if (isArray) {
+            changes.forEach(item => classColors[item.cls] = Color(item.color));
+        } else
+            classColors[changes.cls] = Color(changes.color);
+
+        const conformerColors: NtCColors.Conformers = {
             ...this.state.display.conformerColors,
-            ...this.classColorToConformers(k as keyof ConformersByClass, clr),
+            ...(isArray
+                ? changes.map(item => this.classColorToConformers(item.cls, Color(item.color)))
+                : this.classColorToConformers(changes.cls, Color(changes.color)))
         };
 
         const display = { ...this.state.display, classColors, conformerColors };
@@ -1015,23 +1076,27 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
         this.setState({ ...this.state, display });
     }
 
-    private updateConformerColor(k: keyof NtCColors.Conformers, color: number) {
+    private updateConformerColor(changes: { conformer: keyof NtCColors.Conformers, color: number }|{ conformer: keyof NtCColors.Conformers, color: number }[]) {
         const conformerColors = { ...this.state.display.conformerColors };
-        conformerColors[k] = Color(color);
+        if (Array.isArray(changes))
+            changes.forEach(item => conformerColors[item.conformer] = Color(item.color));
+        else
+            conformerColors[changes.conformer] = Color(changes.color);
 
         const display = { ...this.state.display, conformerColors };
         this.viewer!.changeNtCColors(display);
         this.setState({ ...this.state, display });
     }
 
-    command(cmd: Commands.Cmd) {
+    async command(cmd: Commands.Cmd) {
         if (!this.viewer)
             return;
 
         if (cmd.type === 'redraw')
             window.dispatchEvent(new Event('resize'));
         else if (cmd.type === 'select-step') {
-            this.viewer.superposeReferences(cmd.stepName, cmd.referenceNtC, cmd.references);
+            await this.viewer.superposeReferences(cmd.stepName, cmd.referenceNtC, cmd.references);
+            this.viewer.focusOnSelectedStep();
         } else if (cmd.type === 'switch-model') {
             if (cmd.model < 1 || cmd.model > this.viewer.getModelCount())
                 return;
@@ -1088,213 +1153,203 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
         return (
             <div className='rmsp-app'>
                 <div id={this.props.elemId + '-viewer'} className='rmsp-viewer'></div>
-                <div>
-                    <div
-                        onClick={() => this.setState({ ...this.state, showControls: !this.state.showControls })}
-                    >
-                        Display and control
-                    </div>
-                    {this.state.showControls ?
-                        <div className='rmsp-controls'>
-                            <div className='rmsp-controls-section-caption'>Representation</div>
-                            <div className='rmsp-controls-line'>
+                <CollapsibleVertical caption={'Controls'}>
+                    <div className='rmsp-controls'>
+                        <div className='rmsp-controls-section-caption'>Representation</div>
+                        <div className='rmsp-controls-line'>
+                            <div className='rmsp-control-item'>
+                                <PushButton
+                                    text={capitalize(this.state.display.representation)}
+                                    enabled={ready}
+                                    onClick={() => {
+                                        const display: Display = {
+                                            ...this.state.display,
+                                            representation: this.state.display.representation === 'cartoon' ? 'ball-and-stick' : 'cartoon',
+                                        };
+                                        this.viewer!.changeRepresentation(display);
+                                        this.setState({ ...this.state, display });
+                                    }}
+                                />
+                            </div>
+                        </div>
+
+                        <div className='rmsp-controls-section-caption'>Substructure parts</div>
+                        <div className='rmsp-controls-line'>
+                            <div className='rmsp-control-item'>
+                                <ToggleButton
+                                    text='Nucleic'
+                                    enabled={hasNucleic}
+                                    switchedOn={this.state.display.showNucleic}
+                                    onClick={() => {
+                                        const display: Display = {
+                                            ...this.state.display,
+                                            showNucleic: !this.state.display.showNucleic,
+                                        };
+                                        this.viewer!.toggleSubstructure('nucleic', display);
+                                        this.setState({ ...this.state, display });
+                                    }}
+                                />
+                            </div>
+                            <div className='rmsp-control-item'>
+                                <ToggleButton
+                                    text='Protein'
+                                    enabled={hasProtein}
+                                    switchedOn={this.state.display.showProtein}
+                                    onClick={() => {
+                                        const display: Display = {
+                                            ...this.state.display,
+                                            showProtein: !this.state.display.showProtein,
+                                        };
+                                        this.viewer!.toggleSubstructure('protein', display);
+                                        this.setState({ ...this.state, display });
+                                    }}
+                                />
+                            </div>
+                            <div className='rmsp-control-item'>
+                                <ToggleButton
+                                    text='Water'
+                                    enabled={hasWater}
+                                    switchedOn={this.state.display.showWater}
+                                    onClick={() => {
+                                        const display: Display = {
+                                            ...this.state.display,
+                                            showWater: !this.state.display.showWater,
+                                        };
+                                        this.viewer!.toggleSubstructure('water', display);
+                                        this.setState({ ...this.state, display });
+                                    }}
+                                />
+                            </div>
+                        </div>
+
+                        <div className='rmsp-controls-section-caption'>NtC visuals</div>
+                        <div className='rmsp-controls-line'>
+                            <div className='rmsp-control-item-group'>
                                 <div className='rmsp-control-item'>
-                                    <PushButton
-                                        text={capitalize(this.state.display.representation)}
+                                    <ToggleButton
+                                        text='Pyramids'
                                         enabled={ready}
+                                        switchedOn={this.state.display.showPyramids}
                                         onClick={() => {
                                             const display: Display = {
                                                 ...this.state.display,
-                                                representation: this.state.display.representation === 'cartoon' ? 'ball-and-stick' : 'cartoon',
+                                                showPyramids: !this.state.display.showPyramids,
                                             };
-                                            this.viewer!.changeRepresentation(display);
+                                            this.viewer!.changePyramids(display);
                                             this.setState({ ...this.state, display });
                                         }}
                                     />
                                 </div>
-                            </div>
-
-                            <div className='rmsp-controls-section-caption'>Substructure parts</div>
-                            <div className='rmsp-controls-line'>
                                 <div className='rmsp-control-item'>
-                                    <ToggleButton
-                                        text='Nucleic'
-                                        enabled={hasNucleic}
-                                        switchedOn={this.state.display.showNucleic}
+                                    <PushButton
+                                        text={this.state.display.pyramidsTransparent ? 'Transparent' : 'Solid'}
+                                        enabled={this.state.display.showPyramids}
                                         onClick={() => {
                                             const display: Display = {
                                                 ...this.state.display,
-                                                showNucleic: !this.state.display.showNucleic,
+                                                pyramidsTransparent: !this.state.display.pyramidsTransparent,
                                             };
-                                            this.viewer!.toggleSubstructure('nucleic', display);
+                                            this.viewer!.changePyramids(display);
                                             this.setState({ ...this.state, display });
                                         }}
                                     />
                                 </div>
+                            </div>
+                            <div className='rmsp-control-item-group'>
                                 <div className='rmsp-control-item'>
                                     <ToggleButton
-                                        text='Protein'
-                                        enabled={hasProtein}
-                                        switchedOn={this.state.display.showProtein}
-                                        onClick={() => {
-                                            const display: Display = {
-                                                ...this.state.display,
-                                                showProtein: !this.state.display.showProtein,
-                                            };
-                                            this.viewer!.toggleSubstructure('protein', display);
-                                            this.setState({ ...this.state, display });
-                                        }}
+                                        text='Balls'
+                                        enabled={false}
+                                        switchedOn={false}
+                                        onClick={() => {}}
                                     />
                                 </div>
                                 <div className='rmsp-control-item'>
-                                    <ToggleButton
-                                        text='Water'
-                                        enabled={hasWater}
-                                        switchedOn={this.state.display.showWater}
+                                    <PushButton
+                                        text={this.state.display.ballsTransparent ? 'Transparent' : 'Solid'}
+                                        enabled={this.state.display.showBalls}
                                         onClick={() => {
                                             const display: Display = {
                                                 ...this.state.display,
-                                                showWater: !this.state.display.showWater,
+                                                ballsTransparent: !this.state.display.ballsTransparent,
                                             };
-                                            this.viewer!.toggleSubstructure('water', display);
+                                            /* No balls today... */
                                             this.setState({ ...this.state, display });
                                         }}
                                     />
                                 </div>
                             </div>
+                        </div>
 
-                            <div className='rmsp-controls-section-caption'>NtC visuals</div>
-                            <div className='rmsp-controls-line'>
-                                <div className='rmsp-control-item-group'>
-                                    <div className='rmsp-control-item'>
-                                        <ToggleButton
-                                            text='Pyramids'
-                                            enabled={ready}
-                                            switchedOn={this.state.display.showPyramids}
-                                            onClick={() => {
-                                                const display: Display = {
-                                                    ...this.state.display,
-                                                    showPyramids: !this.state.display.showPyramids,
-                                                };
-                                                this.viewer!.changePyramids(display);
-                                                this.setState({ ...this.state, display });
-                                            }}
-                                        />
-                                    </div>
-                                    <div className='rmsp-control-item'>
-                                        <PushButton
-                                            text={this.state.display.pyramidsTransparent ? 'Transparent' : 'Solid'}
-                                            enabled={this.state.display.showPyramids}
-                                            onClick={() => {
-                                                const display: Display = {
-                                                    ...this.state.display,
-                                                    pyramidsTransparent: !this.state.display.pyramidsTransparent,
-                                                };
-                                                this.viewer!.changePyramids(display);
-                                                this.setState({ ...this.state, display });
-                                            }}
-                                        />
-                                    </div>
-                                </div>
-                                <div className='rmsp-control-item-group'>
-                                    <div className='rmsp-control-item'>
-                                        <ToggleButton
-                                            text='Balls'
-                                            enabled={false}
-                                            switchedOn={false}
-                                            onClick={() => {}}
-                                        />
-                                    </div>
-                                    <div className='rmsp-control-item'>
-                                        <PushButton
-                                            text={this.state.display.ballsTransparent ? 'Transparent' : 'Solid'}
-                                            enabled={this.state.display.showBalls}
-                                            onClick={() => {
-                                                const display: Display = {
-                                                    ...this.state.display,
-                                                    ballsTransparent: !this.state.display.ballsTransparent,
-                                                };
-                                                /* No balls today... */
-                                                this.setState({ ...this.state, display });
-                                            }}
-                                        />
+                        <div className='rmsp-controls-section-caption'>NtC classes colors</div>
+                        <div className='rmsp-controls-line'>
+                            {(['A', 'B', 'BII', 'miB', 'Z', 'IC', 'OPN', 'SYN', 'N'] as (keyof NtCColors.Classes)[]).map(k =>
+                                <div className='rmsp-control-item-group' key={k}>
+                                    <div
+                                        className='rmsp-control-item'
+                                        onClick={evt => ColorPicker.create(
+                                            evt,
+                                            this.state.display.classColors[k],
+                                            color => this.updateClassColor({ cls: k, color })
+                                        )}
+                                    >
+                                        <ColorBox caption={k} color={this.state.display.classColors[k]} />
                                     </div>
+                                    <PushButton
+                                        text='R'
+                                        onClick={() => this.updateClassColor({ cls: k, color: NtCColors.Classes[k] })}
+                                        enabled={true}
+                                    />
                                 </div>
-                            </div>
-
-                            <div className='rmsp-controls-section-caption'>NtC classes colors</div>
-                            <div className='rmsp-controls-line'>
-                                {(['A', 'B', 'BII', 'miB', 'Z', 'IC', 'OPN', 'SYN', 'N'] as (keyof NtCColors.Classes)[]).map(k =>
-                                    <div className='rmsp-control-item-group' key={k}>
-                                        <div
-                                            className='rmsp-control-item'
-                                            onClick={evt => ColorPicker.create(
-                                                evt,
-                                                this.state.display.classColors[k],
-                                                color => this.updateClassColor(k, color)
-                                            )}
-                                        >
-                                            <ColorBox caption={k} color={this.state.display.classColors[k]} />
-                                        </div>
-                                        <PushButton
-                                            text='R'
-                                            onClick={() => this.updateClassColor(k, NtCColors.Classes[k])}
-                                            enabled={true}
-                                        />
-                                    </div>
-                                )}
-                            </div>
+                            )}
+                        </div>
 
-                            <div className='rmsp-controls-section-caption'>NtC colors</div>
-                            <div className='rmsp-controls-line'>
-                                {this.presentConformers.map(ntc => {
-                                    const uprKey = ntc + '_Upr' as keyof NtCColors.Conformers;
-                                    const lwrKey = ntc + '_Lwr' as keyof NtCColors.Conformers;
-
-                                    return (
-                                        <div className='rmsp-control-item' key={ntc}>
-                                            <div className='rmsp-control-item-group'>
-                                                <div
-                                                    className='rmsp-control-item'
-                                                    onClick={evt => ColorPicker.create(
-                                                        evt,
-                                                        this.state.display.conformerColors[uprKey],
-                                                        color => this.updateConformerColor(uprKey, color)
-                                                    )}
-                                                >
-                                                    <ColorBox caption={`${ntc} Upr`} color={this.state.display.conformerColors[uprKey]} />
-                                                </div>
-                                                <PushButton
-                                                    text='R'
-                                                    onClick={() => this.updateConformerColor(uprKey, NtCColors.Conformers[uprKey])}
-                                                    enabled={true}
-                                                />
+                        <div className='rmsp-controls-section-caption'>NtC colors</div>
+                        <div className='rmsp-controls-line'>
+                            {this.presentConformers.map(ntc => {
+                                const uprKey = ntc + '_Upr' as keyof NtCColors.Conformers;
+                                const lwrKey = ntc + '_Lwr' as keyof NtCColors.Conformers;
+
+                                return (
+                                    <div className='rmsp-control-item' key={ntc}>
+                                        <div className='rmsp-control-item-group'>
+                                            <div
+                                                className='rmsp-control-item'
+                                                onClick={evt => ColorPicker.create(
+                                                    evt,
+                                                    this.state.display.conformerColors[uprKey],
+                                                    color => this.updateConformerColor({ conformer: uprKey, color })
+                                                )}
+                                            >
+                                                <ColorBox caption={`${ntc.slice(0, 2)}`} color={this.state.display.conformerColors[uprKey]} />
                                             </div>
-                                            <div className='rmsp-control-item-group'>
-                                                <div
-                                                    className='rmsp-control-item'
-                                                    onClick={evt => ColorPicker.create(
-                                                        evt,
-                                                        this.state.display.conformerColors[lwrKey],
-                                                        color => this.updateConformerColor(lwrKey, color)
-                                                    )}
-                                                >
-                                                    <ColorBox caption={`${ntc} Lwr`} color={this.state.display.conformerColors[lwrKey]} />
-                                                </div>
-                                                <PushButton
-                                                    text='R'
-                                                    onClick={() => this.updateConformerColor(lwrKey, NtCColors.Conformers[lwrKey])}
-                                                    enabled={true}
-                                                />
+                                            <div
+                                                className='rmsp-control-item'
+                                                onClick={evt => ColorPicker.create(
+                                                    evt,
+                                                    this.state.display.conformerColors[lwrKey],
+                                                    color => this.updateConformerColor({ conformer: lwrKey, color })
+                                                )}
+                                            >
+                                                <ColorBox caption={`${ntc.slice(2)}`} color={this.state.display.conformerColors[lwrKey]} />
                                             </div>
+                                            <PushButton
+                                                text='R'
+                                                onClick={() => {
+                                                    this.updateConformerColor([
+                                                        { conformer: uprKey, color: NtCColors.Conformers[uprKey] },
+                                                        { conformer: lwrKey, color: NtCColors.Conformers[lwrKey] }
+                                                    ]);
+                                                }}
+                                                enabled={true}
+                                            />
                                         </div>
-                                    );
-                                })}
-                            </div>
+                                    </div>
+                                );
+                            })}
                         </div>
-                        : undefined
-                    }
-                </div>
+                    </div>
+                </CollapsibleVertical>
             </div>
         );
     }
diff --git a/src/apps/rednatco/rednatco-molstar.css b/src/apps/rednatco/rednatco-molstar.css
index 8b25615cb..c048b92c0 100644
--- a/src/apps/rednatco/rednatco-molstar.css
+++ b/src/apps/rednatco/rednatco-molstar.css
@@ -36,12 +36,22 @@
     line-height: 1.5;
 }
 
-.rmsp-color-box {
-    display: flex;
+.rmsp-collapsible-vertical-caption {
+    background-color: var(--color-f);
+    color: var(--color-c);
+    padding: calc(0.5 * var(--x-gap));
+    transition: background-color var(--anim-speed);
+}
+.rmsp-collapsible-vertical-caption:hover {
+    background-color: var(--color-c);
+    color: black;
 }
 
-.rmsp-color-box-color {
-    flex: 1;
+.rmsp-color-box {
+    align-items: center;
+    display: flex;
+    height: 100%;
+    justify-content: center;
 }
 
 .rmsp-color-picker-nest {
@@ -51,10 +61,12 @@
 }
 
 .rmsp-controls {
+    align-items: center;
     display: grid;
     grid-template-columns: auto 1fr;
     grid-column-gap: var(--h-gap);
-    overflow: scroll;
+    overflow: auto;
+    padding: calc(0.5 * var(--x-gap));
 }
 
 .rmsp-control-item-group {
@@ -96,7 +108,7 @@
 }
 
 .rmsp-togglebutton-switched-on {
-    background-color: green;
+    background-color: #26e326;
 }
 
 .rmsp-pushbutton-text {
diff --git a/src/apps/rednatco/util.ts b/src/apps/rednatco/util.ts
index ee4f8f7bb..75a79ad26 100644
--- a/src/apps/rednatco/util.ts
+++ b/src/apps/rednatco/util.ts
@@ -1,13 +1,10 @@
-import { OrderedSet } from '../../mol-data/int/ordered-set';
-import { ElementIndex, StructureElement, Unit } from '../../mol-model/structure';
+import { Color } from '../../mol-util/color';
 
-export function lociElements(loci: StructureElement.Loci) {
-    const es = loci.elements[0]; // Ignore anything but the first chuck
-
-    if (!Unit.isAtomic(es.unit))
-        return [];
-
-    const elems = new Array<ElementIndex>();
-    OrderedSet.forEach(es.indices, uI => elems.push(es.unit.elements[uI]));
-    return elems;
+/* https://alienryderflex.com/hsp.html */
+export function luminance(color: Color) {
+    let [r, g, b] = Color.toRgb(color);
+    r /= 255.0;
+    g /= 255.0;
+    b /= 255.0;
+    return Math.sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);
 }
diff --git a/webpack.config.common.js b/webpack.config.common.js
index f674b9d3f..a9e612866 100644
--- a/webpack.config.common.js
+++ b/webpack.config.common.js
@@ -23,6 +23,16 @@ const sharedConfig = {
                     options: { name: '[name].[ext]' }
                 }]
             },
+            {
+                test: /\.svg$/,
+                use: [{
+                    loader: 'file-loader',
+                    options: {
+                        outputPath: 'assets/imgs',
+                        name: '[name].[ext]'
+                    }
+                }]
+            },
             {
                 test: /\.(s*)css$/,
                 use: [
-- 
GitLab