diff --git a/src/apps/rednatco/api-impl.ts b/src/apps/rednatco/api-impl.ts
new file mode 100644
index 0000000000000000000000000000000000000000..beb2689f220c9a7725a1c92fca3f677771d40070
--- /dev/null
+++ b/src/apps/rednatco/api-impl.ts
@@ -0,0 +1,46 @@
+import { ReDNATCOMsp } from './index';
+import { ReDNATCOMspApi } from './api';
+
+export class ReDNATCOMspApiImpl implements ReDNATCOMspApi.Object {
+    private target: ReDNATCOMsp|undefined = undefined;
+    private onEvent: ((evt: ReDNATCOMspApi.Event) => void)|undefined;
+
+    private check() {
+        if (!this.target)
+            throw new Error('ReDNATCOMsp object not bound');
+    }
+
+    _bind(target: ReDNATCOMsp) {
+        this.target = target;
+    }
+
+    command(cmd: ReDNATCOMspApi.Command) {
+        this.check();
+        this.target!.command(cmd);
+    }
+
+    event(evt: ReDNATCOMspApi.Event) {
+        if (this.onEvent)
+            this.onEvent(evt);
+    }
+
+    init(elemId: string, onEvent?: (evt: ReDNATCOMspApi.Event) => void, onInited?: () => void) {
+        this.onEvent = onEvent;
+        ReDNATCOMsp.init(elemId, onInited);
+        return this;
+    }
+
+    isReady(): boolean {
+        return !!this.target;
+    }
+
+    loadStructure(data: string, type: 'cif'|'pdb') {
+        this.check();
+        this.target!.loadStructure(data, type);
+    }
+
+    query(type: ReDNATCOMspApi.Queries.Type): ReDNATCOMspApi.Response {
+        this.check();
+        return this.target!.apiQuery(type);
+    }
+}
diff --git a/src/apps/rednatco/api.ts b/src/apps/rednatco/api.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b6267aef5fb30478a54696c9ad833f627bf1dd8e
--- /dev/null
+++ b/src/apps/rednatco/api.ts
@@ -0,0 +1,86 @@
+export namespace ReDNATCOMspApi {
+    export namespace Commands {
+        export type Type = 'deselect-step'|'redraw'|'select-step'|'switch-model';
+
+        export type Redraw = { type: 'redraw' }
+        export function Redraw(): Redraw { return { type: 'redraw' }; }
+
+        export type DeselectStep = { type: 'deselect-step' }
+        export function DeselectStep() {
+            return { type: 'deselect-step' };
+        }
+
+        export type SelectStep = {
+            type: 'select-step';
+            stepName: string;
+            prevStepName: string|undefined;
+            nextStepName: string|undefined;
+            referenceNtC: string;
+            references: ('sel'|'prev'|'next')[];
+        }
+        export function SelectStep(stepName: string, prevStepName: string|undefined, nextStepName: string|undefined, referenceNtC = '', references = ['sel', 'prev', 'next']): SelectStep {
+            return {
+                type: 'select-step',
+                stepName,
+                prevStepName,
+                nextStepName,
+                referenceNtC,
+                references: references as ('sel'|'prev'|'next')[],
+            };
+        }
+
+        export type SwitchModel = { type: 'switch-model', model: number };
+        export function SwitchModel(model: number): SwitchModel { return { type: 'switch-model', model }; }
+    }
+    export type Command =
+        Commands.DeselectStep |
+        Commands.Redraw |
+        Commands.SelectStep |
+        Commands.SwitchModel;
+
+    export namespace Events {
+        export type Type = 'step-deselected'|'step-requested'|'step-selected';
+
+        export type StepDeselected = { type: 'step-deselected' }
+        export function StepDeselected(): StepDeselected {
+            return { type: 'step-deselected' };
+        }
+
+        export type StepRequested = { type: 'step-requested', name: string }
+        export function StepRequested(name: string): StepRequested {
+            return { type: 'step-requested', name };
+        }
+
+        export type StepSelected = { type: 'step-selected', success: boolean, name: string, rmsd?: number }
+        export function StepSelectedOk(name: string, rmsd?: number): StepSelected {
+            return { type: 'step-selected', success: true, name, rmsd };
+        }
+        export function StepSelectedFail(): StepSelected {
+            return { type: 'step-selected', success: false, name: '' };
+        }
+
+    }
+    export type Event =
+        Events.StepDeselected |
+        Events.StepRequested |
+        Events.StepSelected;
+
+    export namespace Queries {
+        export type Type = 'selected-step';
+
+        export type SelectedStep = { type: 'selected-step', name: string, rmsd?: number }
+        export function SelectedStep(name: string, rmsd?: number): SelectedStep {
+            return { type: 'selected-step', name, rmsd };
+        }
+    }
+    export type Response = Queries.SelectedStep;
+
+    export interface Object {
+        command: (cmd: Command) => void;
+        event: (evt: Event) => void;
+        init: (elemId: string, onEvent?: (evt: Event) => void, onInited?: () => void) => void;
+        isReady: () => boolean;
+        loadStructure: (data: string, type: 'cif'|'pdb') => void;
+        query: (type: Queries.Type) => Response;
+    }
+}
diff --git a/src/apps/rednatco/commands.ts b/src/apps/rednatco/commands.ts
deleted file mode 100644
index 7c48b0ce6e93e562e3d569ee03d241ef6ef19b19..0000000000000000000000000000000000000000
--- a/src/apps/rednatco/commands.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export namespace Commands {
-    export type Type = 'redraw'|'select-step'|'switch-model';
-
-    export type Redraw = { type: 'redraw' }
-    export function Redraw(): Redraw { return { type: 'redraw' }; }
-
-    export type SelectStep = {
-        type: 'select-step';
-        stepName: string;
-        prevStepName: string|null;
-        nextStepName: string|null;
-        referenceNtC: string;
-        references: ('sel'|'prev'|'next')[];
-    }
-    export function SelectStep(stepName: string, prevStepName: string|null, nextStepName: string|null, referenceNtC = '', references = ['sel', 'prev', 'next']): SelectStep {
-        return {
-            type: 'select-step',
-            stepName,
-            prevStepName,
-            nextStepName,
-            referenceNtC,
-            references: references as ('sel'|'prev'|'next')[],
-        };
-    }
-
-    export type SwitchModel = { type: 'switch-model', model: number };
-    export function SwitchModel(model: number): SwitchModel { return { type: 'switch-model', model }; }
-
-    export type Cmd = Redraw|SelectStep|SwitchModel;
-}
diff --git a/src/apps/rednatco/idents.ts b/src/apps/rednatco/idents.ts
index 6340bfb810f5e653945aa62adcb740a38c659155..c1f27f8b6aa902826e83fa4e711bda8f70d5e453 100644
--- a/src/apps/rednatco/idents.ts
+++ b/src/apps/rednatco/idents.ts
@@ -1,5 +1,5 @@
 export type ID ='data'|'trajectory'|'model'|'structure'|'visual'|'pyramids'|'superposition';
-export type Substructure = 'nucleic'|'protein'|'water';
+export type Substructure = 'nucleic'|'protein'|'water'|'selected-slice'|'remainder-slice';
 
 export function ID(id: ID, sub: Substructure|'', ref: string) {
     if (sub === '')
diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx
index 20925066fa183c433dfb20648e0ca8fdbc9a6c16..c37fcbd2ad4c6f38a695fb2b7361c62c035d6e65 100644
--- a/src/apps/rednatco/index.tsx
+++ b/src/apps/rednatco/index.tsx
@@ -1,62 +1,14 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
+import { ReDNATCOMspApi as Api } from './api';
+import { ReDNATCOMspApiImpl } from './api-impl';
+import { ReDNATCOMspViewer } from './viewer';
 import { NtCColors } from './colors';
 import { ColorPicker } from './color-picker';
-import { Commands } from './commands';
 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 { 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';
-import { PluginBehavior, PluginBehaviors } from '../../mol-plugin/behavior';
-import { PluginCommands } from '../../mol-plugin/commands';
-import { PluginContext } from '../../mol-plugin/context';
-import { PluginSpec } from '../../mol-plugin/spec';
-import { LociLabel } from '../../mol-plugin-state/manager/loci-label';
-import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
-import { StateTransforms } from '../../mol-plugin-state/transforms';
-import { RawData } from '../../mol-plugin-state/transforms/data';
-import { createPluginUI } from '../../mol-plugin-ui';
-import { PluginUIContext } from '../../mol-plugin-ui/context';
-import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
-import { Representation } from '../../mol-repr/representation';
-import { StateObjectCell, StateObject, StateSelection } from '../../mol-state';
-import { StateTreeSpine } from '../../mol-state/tree/spine';
-import { lociLabel } from '../../mol-theme/label';
 import { Color } from '../../mol-util/color';
-import { arrayMax } from '../../mol-util/array';
-import { Binding } from '../../mol-util/binding';
-import { ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
-import { MarkerAction } from '../../mol-util/marker-action';
-import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { ObjectKeys } from '../../mol-util/type-helpers';
 import './index.html';
-import './molstar.css';
-import './rednatco-molstar.css';
-
-const Extensions = {
-    'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
-};
-
-const AnimationDurationMsec = 150;
-const BaseRef = 'rdo';
-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'],
@@ -71,22 +23,7 @@ const ConformersByClass = {
 };
 type ConformersByClass = typeof ConformersByClass;
 
-const DefaultChainColor = Color(0xD9D9D9);
-
-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>
-        );
-    }
-}
-
+export type VisualRepresentations = 'ball-and-stick'|'cartoon';
 const Display = {
     representation: 'cartoon' as VisualRepresentations,
 
@@ -105,19 +42,7 @@ const Display = {
     classColors: { ...NtCColors.Classes },
     conformerColors: { ...NtCColors.Conformers },
 };
-type Display = typeof Display;
-
-type StepInfo = {
-    name: string;
-    assignedNtC: string;
-    closestNtC: string; // Fallback for cases where assignedNtC is NANT
-    resNo1: number;
-    resNo2: number;
-    altId1?: string;
-    altId2?: string;
-}
-
-type VisualRepresentations = 'ball-and-stick'|'cartoon';
+export type Display = typeof Display;
 
 function capitalize(s: string) {
     if (s.length === 0)
@@ -125,904 +50,17 @@ function capitalize(s: string) {
     return s[0].toLocaleUpperCase() + s.slice(1);
 }
 
-function dinucleotideBackbone(loci: StructureElement.Loci, altId1?: string, altId2?: string) {
-    const es = loci.elements[0];
-    const loc = Location.create(loci.structure, es.unit, es.unit.elements[OrderedSet.getAt(es.indices, 0)]);
-    const len = OrderedSet.size(es.indices);
-    const indices = new Array<ElementIndex>();
-
-    const gather = (atoms: string[], start: number, end: number, altId?: string) => {
-        for (const atom of atoms) {
-            let idx = start;
-            for (; idx < end; idx++) {
-                loc.element = es.unit.elements[OrderedSet.getAt(es.indices, idx)];
-                const _atom = StructureProperties.atom.label_atom_id(loc);
-                if (atom === _atom) {
-                    if (altId) {
-                        const _altId = StructureProperties.atom.label_alt_id(loc);
-                        if (_altId !== '' && _altId !== altId)
-                            continue;
-                    }
-
-                    indices.push(loc.element);
-                    break;
-                }
-            }
-            if (idx === end) {
-                console.error(`Cannot find backbone atom ${atom} in first residue of a step`);
-                return false;
-            }
-        }
-
-        return true;
-    };
-
-    // Find split between first and second residue
-    const resNo1 = StructureProperties.residue.auth_seq_id(loc);
-    let secondIdx = -1;
-    for (let idx = 0; idx < len; idx++) {
-        loc.element = es.unit.elements[OrderedSet.getAt(es.indices, idx)];
-        const resNo = StructureProperties.residue.auth_seq_id(loc);
-        if (resNo !== resNo1) {
-            secondIdx = idx;
-            break;
-        }
-    }
-    if (secondIdx === -1)
-        return [];
-
-    // Gather ElementIndices for backbone atoms of the first  residue
-    loc.element = es.unit.elements[OrderedSet.getAt(es.indices, 0)];
-    const ring1 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
-    if (!ring1)
-        return [];
-
-    const first = RefCfmr.BackboneAtoms.first.concat(RefCfmr.BackboneAtoms[ring1]);
-    if (!gather(first, 0, secondIdx, altId1))
-        return [];
-
-    loc.element = es.unit.elements[OrderedSet.getAt(es.indices, secondIdx)];
-    const ring2 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
-    if (!ring2)
-        return [];
-
-    const second = RefCfmr.BackboneAtoms.second.concat(RefCfmr.BackboneAtoms[ring2]);
-    if (!gather(second, secondIdx, len, altId2))
-        return [];
-
-    return indices;
-}
-
-function rcref(c: string, where: 'sel'|'prev'|'next'|'' = '') {
-    return `${RCRef}-${c}-${where}`;
-}
-
-const ReDNATCOLociLabelProvider = PluginBehavior.create({
-    name: 'watlas-loci-label-provider',
-    category: 'interaction',
-    ctor: class implements PluginBehavior<undefined> {
-        private f = {
-            label: (loci: Loci) => {
-                switch (loci.kind) {
-                    case 'structure-loci':
-                    case 'element-loci':
-                        return lociLabel(loci);
-                    default:
-                        return '';
-                }
-            },
-            group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'),
-            priority: 100
-        };
-        register() { this.ctx.managers.lociLabels.addProvider(this.f); }
-        unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
-        constructor(protected ctx: PluginContext) { }
-    },
-    display: { name: 'ReDNATCO labeling' }
-});
-
-const ReDNATCOLociSelectionBindings = {
-    clickFocus: Binding([Binding.Trigger(ButtonsType.Flag.Secondary)], 'Focus camera on selected loci using ${triggers}'),
-    clickSelectOnly: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Select the clicked element using ${triggers}.'),
-    clickDeselectAllOnEmpty: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Deselect all when clicking on nothing using ${triggers}.'),
-};
-const ReDNATCOLociSelectionParams = {
-    bindings: PD.Value(ReDNATCOLociSelectionBindings, { isHidden: true }),
-    onDeselected: PD.Value(() => {}, { isHidden: true }),
-    onSelected: PD.Value((loci: Representation.Loci) => {}, { isHidden: true }),
-};
-type ReDNATCOLociSelectionProps = PD.Values<typeof ReDNATCOLociSelectionParams>;
-
-const ReDNATCOLociSelectionProvider = PluginBehavior.create({
-    name: 'rednatco-loci-selection-provider',
-    category: 'interaction',
-    display: { name: 'Interactive step selection' },
-    params: () => ReDNATCOLociSelectionParams,
-    ctor: class extends PluginBehavior.Handler<ReDNATCOLociSelectionProps> {
-        private spine: StateTreeSpine.Impl;
-        private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
-            if (!this.ctx.canvas3d) return;
-            this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
-        };
-        private applySelectMark(ref: string, clear?: boolean) {
-            const cell = this.ctx.state.data.cells.get(ref);
-            if (cell && PSO.isRepresentation3D(cell.obj)) {
-                this.spine.current = cell;
-                const so = this.spine.getRootOfType(PSO.Molecule.Structure);
-                if (so) {
-                    if (clear) {
-                        this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
-                    }
-                    const loci = this.ctx.managers.structure.selection.getLoci(so.data);
-                    this.lociMarkProvider({ loci }, MarkerAction.Select);
-                }
-            }
-        }
-        private focusOnLoci(loci: Representation.Loci) {
-            if (!this.ctx.canvas3d)
-                return;
-
-            const sphere = Loci.getBoundingSphere(loci.loci);
-            if (!sphere)
-                return;
-            const snapshot = this.ctx.canvas3d.camera.getSnapshot();
-            snapshot.target = sphere.center;
-
-            PluginCommands.Camera.SetSnapshot(this.ctx, { snapshot, durationMs: AnimationDurationMsec });
-        }
-        register() {
-            const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci);
-            const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci);
-
-            const actions: [keyof typeof ReDNATCOLociSelectionBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
-                ['clickFocus', current => this.focusOnLoci(current), lociIsNotEmpty],
-                [
-                    'clickDeselectAllOnEmpty',
-                    () => {
-                        this.ctx.managers.interactivity.lociSelects.deselectAll();
-                        this.params.onDeselected();
-                    },
-                    lociIsEmpty
-                ],
-                [
-                    'clickSelectOnly',
-                    current => {
-                        this.ctx.managers.interactivity.lociSelects.deselectAll();
-                        if (current.loci.kind === 'element-loci') {
-                            this.ctx.managers.interactivity.lociSelects.select(current);
-                            this.params.onSelected(current);
-                        }
-                    },
-                    lociIsNotEmpty
-                ],
-            ];
-
-            // sort the action so that the ones with more modifiers trigger sooner.
-            actions.sort((a, b) => {
-                const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
-                const k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(t => ModifiersKeys.size(t.modifiers)));
-                const l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(t => ModifiersKeys.size(t.modifiers)));
-                return l - k;
-            });
-
-            this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
-                if (!this.ctx.canvas3d) return;
-
-                // only trigger the 1st action that matches
-                for (const [binding, action, condition] of actions) {
-                    if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
-                        action(current);
-                        break;
-                    }
-                }
-            });
-
-            this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
-
-            this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
-
-            // re-apply select-mark to all representation of an updated structure
-            this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, oldData, action }) => {
-                const cell = this.ctx.state.data.cells.get(ref);
-                if (cell && PSO.Molecule.Structure.is(cell.obj)) {
-                    const structure: Structure = obj.data;
-                    const oldStructure: Structure | undefined = action === 'recreate' ? oldObj?.data :
-                        action === 'in-place' ? oldData : undefined;
-                    if (oldStructure &&
-                        Structure.areEquivalent(structure, oldStructure) &&
-                        Structure.areHierarchiesEqual(structure, oldStructure)) return;
-
-                    const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(PSO.Molecule.Structure.Representation3D, ref));
-                    for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);
-                }
-            });
-
-        }
-        unregister() {
-        }
-        constructor(ctx: PluginContext, params: ReDNATCOLociSelectionProps) {
-            super(ctx, params);
-            this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
-        }
-    },
-});
-
-class ReDNATCOMspViewer {
-    private haveMultipleModels = false;
-    private steps: StepInfo[] = [];
-    private stepNames: Map<string, number> = new Map();
-
-    constructor(public plugin: PluginUIContext, interactionContext: { self?: ReDNATCOMspViewer }) {
-        interactionContext.self = this;
-    }
-
-    private currentModelNumber() {
-        const model = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
-        if (!model)
-            return -1;
-        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));
-    }
-
-    private getStructureParent(cell: StateObjectCell) {
-        if (!cell.sourceRef)
-            return undefined;
-        const parent = this.plugin.state.data.cells.get(cell.sourceRef);
-        if (!parent)
-            return undefined;
-        return parent.obj?.type.name === 'Structure' ? parent.obj : undefined;
-    }
-
-    private ntcRef(step: StepInfo, where: 'sel'|'prev'|'next') {
-        return rcref(step.assignedNtC === 'NANT' ? step.closestNtC : step.assignedNtC, where);
-    }
-
-    private pyramidsParams(colors: NtCColors.Conformers, visible: Map<string, boolean>, transparent: boolean) {
-        const typeParams = {} as PD.Values<ConfalPyramidsParams>;
-        for (const k of Reflect.ownKeys(ConfalPyramidsParams) as (keyof ConfalPyramidsParams)[]) {
-            if (ConfalPyramidsParams[k].type === 'boolean')
-                (typeParams[k] as any) = visible.get(k) ?? ConfalPyramidsParams[k]['defaultValue'];
-        }
-
-        return {
-            type: { name: 'confal-pyramids', params: { ...typeParams, alpha: transparent ? 0.5 : 1.0 } },
-            colorTheme: {
-                name: 'confal-pyramids',
-                params: {
-                    colors: {
-                        name: 'custom',
-                        params: colors,
-                    },
-                },
-            },
-        };
-    }
-
-    private resetCameraRadius() {
-        if (!this.plugin.canvas3d)
-            return;
-
-        const spheres = [];
-        for (const [ref, cell] of Array.from(this.plugin.state.data.cells)) {
-            if (!IDs.isVisual(ref))
-                continue;
-            const parent = this.getStructureParent(cell);
-            if (parent) {
-                const loci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(parent.data, parent.data));
-                const s = Loci.getBoundingSphere(loci);
-                if (s)
-                    spheres.push(s);
-            }
-        }
-
-        if (spheres.length === 0)
-            return;
-
-        SphereBoundaryHelper.reset();
-        for (const s of spheres)
-            SphereBoundaryHelper.includePositionRadius(s.center, s.radius);
-        SphereBoundaryHelper.finishedIncludeStep();
-        for (const s of spheres)
-            SphereBoundaryHelper.radiusPositionRadius(s.center, s.radius);
-        const bs = SphereBoundaryHelper.getSphere();
-
-        const snapshot = this.plugin.canvas3d.camera.getSnapshot();
-        snapshot.radius = bs.radius;
-        snapshot.target = bs.center;
-        PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: AnimationDurationMsec });
-    }
-
-    private substructureVisuals(representation: 'ball-and-stick'|'cartoon') {
-        switch (representation) {
-            case 'cartoon':
-                return {
-                    type: {
-                        name: 'cartoon',
-                        params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false },
-                    },
-                    colorTheme: { name: 'uniform', params: { color: DefaultChainColor } }
-                };
-            case 'ball-and-stick':
-                return {
-                    type: {
-                        name: 'ball-and-stick',
-                        params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false },
-                    },
-                    colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'custom', params: DefaultChainColor } } },
-                };
-        }
-    }
-
-    private superpose(reference: StructureElement.Loci, stru: StructureElement.Loci, altId1?: string, altId2?: string) {
-        const refElems = dinucleotideBackbone(reference);
-        const struElems = dinucleotideBackbone(stru, altId1, altId2);
-
-        return Superpose.superposition(
-            { elements: refElems, conformation: reference.elements[0].unit.conformation },
-            { elements: struElems, conformation: stru.elements[0].unit.conformation }
-        );
-    }
-
-    static async create(target: HTMLElement) {
-        const interactCtx: { self?: ReDNATCOMspViewer } = { self: undefined };
-        const defaultSpec = DefaultPluginUISpec();
-        const spec: PluginUISpec = {
-            ...defaultSpec,
-            behaviors: [
-                PluginSpec.Behavior(ReDNATCOLociLabelProvider),
-                PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
-                PluginSpec.Behavior(
-                    ReDNATCOLociSelectionProvider,
-                    {
-                        bindings: ReDNATCOLociSelectionBindings,
-                        onDeselected: () => interactCtx.self!.onDeselected(),
-                        onSelected: (loci) => interactCtx.self!.onLociSelected(loci),
-                    }
-                ),
-                ...ObjectKeys(Extensions).map(k => Extensions[k]),
-            ],
-            components: {
-                ...defaultSpec.components,
-                controls: {
-                    ...defaultSpec.components?.controls,
-                    top: 'none',
-                    right: 'none',
-                    bottom: 'none',
-                    left: 'none'
-                },
-            },
-            layout: {
-                initial: {
-                    isExpanded: false,
-                    showControls: false,
-                },
-            },
-        };
-
-        const plugin = await createPluginUI(target, spec);
-
-        plugin.managers.interactivity.setProps({ granularity: 'two-residues' });
-        plugin.selectionMode = true;
-
-        return new ReDNATCOMspViewer(plugin, interactCtx);
-    }
-
-    async changeNtCColors(display: Partial<Display>) {
-        if (!this.has('pyramids', 'nucleic'))
-            return;
-
-        const b = this.plugin.state.data.build().to(IDs.ID('pyramids', 'nucleic', BaseRef));
-        b.update(
-            StateTransforms.Representation.StructureRepresentation3D,
-            old => ({
-                ...old,
-                colorTheme: {
-                    name: 'confal-pyramids',
-                    params: {
-                        colors: {
-                            name: 'custom',
-                            params: display.conformerColors ?? NtCColors.Conformers,
-                        },
-                    },
-                },
-            })
-        );
-
-        await b.commit();
-    }
-
-    async changePyramids(display: Partial<Display>) {
-        if (display.showPyramids) {
-            if (!this.has('pyramids', 'nucleic')) {
-                const b = this.getBuilder('structure', 'nucleic');
-                if (b) {
-                    b.apply(
-                        StateTransforms.Representation.StructureRepresentation3D,
-                        this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false),
-                        { ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
-                    );
-                    await b.commit();
-                }
-            } else {
-                const b = this.getBuilder('pyramids', 'nucleic');
-                b.update(
-                    StateTransforms.Representation.StructureRepresentation3D,
-                    old => ({
-                        ...old,
-                        ...this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false),
-                    })
-                );
-                await b.commit();
-            }
-        } else
-            await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('pyramids', 'nucleic', BaseRef) });
-    }
-
-    async changeRepresentation(display: Partial<Display>) {
-        const b = this.plugin.state.data.build();
-        const repr = display.representation ?? 'cartoon';
-
-        for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) {
-            if (this.has('visual', sub)) {
-                b.to(IDs.ID('visual', sub, BaseRef))
-                    .update(
-                        StateTransforms.Representation.StructureRepresentation3D,
-                        old => ({
-                            ...old,
-                            ...this.substructureVisuals(repr),
-                        })
-                    );
-            }
-        }
-
-        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)
-            return void 0;
-        const model = (obj as StateObject<Model>);
-        const sourceData = model.data.sourceData;
-        if (!MmcifFormat.is(sourceData))
-            return void 0;
-
-        const tableSum = sourceData.data.frame.categories['ndb_struct_ntc_step_summary'];
-        const tableStep = sourceData.data.frame.categories['ndb_struct_ntc_step'];
-        if (!tableSum || !tableStep) {
-            console.warn('NtC information not present');
-            return void 0;
-        }
-
-        const _ids = tableStep.getField('id');
-        const _names = tableStep.getField('name');
-        const _authSeqId1 = tableStep.getField('auth_seq_id_1');
-        const _authSeqId2 = tableStep.getField('auth_seq_id_2');
-        const _labelAltId1 = tableStep.getField('label_alt_id_1');
-        const _labelAltId2 = tableStep.getField('label_alt_id_2');
-        const _stepIds = tableSum.getField('step_id');
-        const _assignedNtCs = tableSum.getField('assigned_NtC');
-        const _closestNtCs = tableSum.getField('closest_NtC');
-        if (!_ids || !_names || !_stepIds || !_assignedNtCs || !_closestNtCs || !_labelAltId1 || !_labelAltId2 || !_authSeqId1 || !_authSeqId2) {
-            console.warn('Expected fields are not present in NtC categories');
-            return void 0;
-        }
-
-        const ids = _ids.toIntArray();
-        const names = _names.toStringArray();
-        const authSeqId1 = _authSeqId1.toIntArray();
-        const authSeqId2 = _authSeqId2.toIntArray();
-        const labelAltId1 = _labelAltId1.toStringArray();
-        const labelAltId2 = _labelAltId2.toStringArray();
-        const stepIds = _stepIds.toIntArray();
-        const assignedNtCs = _assignedNtCs.toStringArray();
-        const closestNtCs = _closestNtCs.toStringArray();
-        const len = ids.length;
-
-        const stepNames = new Map<string, number>();
-        const steps = new Array<StepInfo>(len);
-
-        for (let idx = 0; idx < len; idx++) {
-            const id = ids[idx];
-            const name = names[idx];
-            for (let jdx = 0; jdx < len; jdx++) {
-                if (stepIds[jdx] === id) {
-                    const assignedNtC = assignedNtCs[jdx];
-                    const closestNtC = closestNtCs[jdx];
-                    const resNo1 = authSeqId1[jdx];
-                    const resNo2 = authSeqId2[jdx];
-                    const altId1 = labelAltId1[jdx] === '' ? void 0 : labelAltId1[jdx];
-                    const altId2 = labelAltId2[jdx] === '' ? void 0 : labelAltId2[jdx];
-
-                    // We're assuming that steps are ID'd with a contigious, monotonic sequence starting from 1
-                    steps[id - 1] = { name, assignedNtC, closestNtC, resNo1, resNo2, altId1, altId2 };
-                    stepNames.set(name, id - 1);
-                    break;
-                }
-            }
-        }
-
-        return { steps, stepNames };
-    }
-
-    getModelCount() {
-        const obj = this.plugin.state.data.cells.get(IDs.ID('trajectory', '', BaseRef))?.obj;
-        if (!obj)
-            return 0;
-        return (obj as StateObject<Trajectory>).data.frameCount;
-    }
-
-    getPresentConformers() {
-        const obj = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
-        if (!obj)
-            return [];
-        const model = (obj as StateObject<Model>);
-        const modelNum = model.data.modelNum;
-        const sourceData = model.data.sourceData;
-        if (MmcifFormat.is(sourceData)) {
-            const tableSum = sourceData.data.frame.categories['ndb_struct_ntc_step_summary'];
-            const tableStep = sourceData.data.frame.categories['ndb_struct_ntc_step'];
-            if (!tableSum || !tableStep) {
-                console.warn('NtC information not present');
-                return [];
-            }
-
-            const _stepIds = tableSum.getField('step_id');
-            const _assignedNtCs = tableSum.getField('assigned_NtC');
-            const _ids = tableStep.getField('id');
-            const _modelNos = tableStep.getField('PDB_model_number');
-            if (!_stepIds || !_assignedNtCs || !_ids || !_modelNos) {
-                console.warn('Expected fields are not present in NtC categories');
-                return [];
-            }
-
-            const stepIds = _stepIds.toIntArray();
-            const assignedNtCs = _assignedNtCs.toStringArray();
-            const ids = _ids.toIntArray();
-            const modelNos = _modelNos.toIntArray();
-
-            const present = new Array<string>();
-            for (let row = 0; row < stepIds.length; row++) {
-                const idx = ids.indexOf(stepIds[row]);
-                if (modelNos[idx] === modelNum) {
-                    const ntc = assignedNtCs[row];
-                    if (!present.includes(ntc))
-                        present.push(ntc);
-                }
-            }
-
-            present.sort();
-            return present;
-        }
-        return [];
-    }
-
-    has(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
-        return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj?.data;
-    }
-
-    isReady() {
-        return this.has('structure', '', BaseRef);
-    }
-
-    async loadStructure(data: string, type: 'pdb'|'cif', display: Partial<Display>) {
-        // TODO: Remove the currently loaded structure
-
-        const b = (t => type === 'pdb'
-            ? t.apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
-            : t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
-        )(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', '', BaseRef) }))
-            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 }, { ref: IDs.ID('model', '', BaseRef) })
-            .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', BaseRef) })
-            // Extract substructures
-            .apply(StateTransforms.Model.StructureComplexElement, { type: 'nucleic' }, { ref: IDs.ID('structure', 'nucleic', BaseRef) })
-            .to(IDs.ID('structure', '', BaseRef))
-            .apply(StateTransforms.Model.StructureComplexElement, { type: 'protein' }, { ref: IDs.ID('structure', 'protein', BaseRef) })
-            .to(IDs.ID('structure', '', BaseRef))
-            .apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: IDs.ID('structure', 'water', BaseRef) });
-        // Commit now so that we can check whether individual substructures are available
-        await b.commit();
-
-        // Create default visuals
-        const bb = this.plugin.state.data.build();
-        if (display.showNucleic && this.has('structure', 'nucleic')) {
-            bb.to(IDs.ID('structure', 'nucleic', BaseRef))
-                .apply(
-                    StateTransforms.Representation.StructureRepresentation3D,
-                    this.substructureVisuals('cartoon'),
-                    { ref: IDs.ID('visual', 'nucleic', BaseRef) }
-                )
-            if (display.showPyramids) {
-                bb.to(IDs.ID('structure', 'nucleic', BaseRef))
-                    .apply(
-                        StateTransforms.Representation.StructureRepresentation3D,
-                        this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), false),
-                        { ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
-                    );
-            }
-        }
-        if (display.showProtein && this.has('structure', 'protein')) {
-            bb.to(IDs.ID('structure', 'protein', BaseRef))
-                .apply(
-                    StateTransforms.Representation.StructureRepresentation3D,
-                    this.substructureVisuals('cartoon'),
-                    { ref: IDs.ID('visual', 'protein', BaseRef) }
-                );
-        }
-        if (display.showWater && this.has('structure', 'water')) {
-            bb.to(IDs.ID('structure', 'water', BaseRef))
-                .apply(
-                    StateTransforms.Representation.StructureRepresentation3D,
-                    this.substructureVisuals('ball-and-stick'),
-                    { ref: IDs.ID('visual', 'water', BaseRef) }
-                );
-        }
-
-        await bb.commit();
-
-        this.haveMultipleModels = this.getModelCount() > 1;
-
-        const ntcInfo = this.gatherStepInfo();
-        if (!ntcInfo) {
-            this.steps.length = 0;
-            this.stepNames.clear();
-        } else {
-            this.steps = ntcInfo.steps;
-            this.stepNames = ntcInfo.stepNames;
-        }
-    }
-
-    async loadReferenceConformers() {
-        const b = this.plugin.state.data.build().toRoot();
-
-        for (const c in ReferenceConformersPdbs) {
-            const cfmr = ReferenceConformersPdbs[c as keyof typeof ReferenceConformersPdbs];
-            const bRef = rcref(c);
-            const mRef = IDs.ID('model', '', bRef);
-            b.toRoot();
-            b.apply(RawData, { data: cfmr, label: `Reference ${c}` }, { ref: IDs.ID('data', '', bRef) })
-                .apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', bRef) })
-                .apply(StateTransforms.Model.ModelFromTrajectory, {}, { ref: mRef })
-                .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'sel')) })
-                .to(mRef)
-                .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'prev')) })
-                .to(mRef)
-                .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'next')) });
-        }
-
-        await b.commit();
-    }
-
-    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();
-    }
-
-    async onLociSelected(selected: Representation.Loci) {
-        const loci = Loci.normalize(selected.loci, 'two-residues');
-
-        if (loci.kind === 'element-loci') {
-            // TODO: This cannot call superposeReferences directly
-            // Instead, we must make a callback via the API
-            // and have the listener decide what to do with this event
-            const stepDesc = Step.describe(loci);
-            if (!stepDesc)
-                return;
-            const stepName = Step.name(stepDesc, this.haveMultipleModels);
-            await this.superposeReferences(stepName, null, null, '', []);
-            this.focusOnSelectedStep();
-        }
-    }
-
-    async switchModel(display: Partial<Display>) {
-        if (display.modelNumber && display.modelNumber === this.currentModelNumber())
-            return;
-
-        const b = this.plugin.state.data.build()
-            .delete(IDs.ID('superposition', '', NtCSupSel))
-            .delete(IDs.ID('superposition', '', NtCSupPrev))
-            .delete(IDs.ID('superposition', '', NtCSupNext))
-            .to(IDs.ID('model', '', BaseRef))
-            .update(
-                StateTransforms.Model.ModelFromTrajectory,
-                old => ({
-                    ...old,
-                    modelIndex: display.modelNumber ? display.modelNumber - 1 : 0
-                })
-            );
-
-        await b.commit();
-    }
-
-    async superposeReferences(stepName: string, prevStepName: string|null, nextStepName: string|null, referenceNtc: string, references: ('sel'|'prev'|'next')[]) {
-        const ReferenceVisuals = (color: number) => {
-            return {
-                type: { name: 'ball-and-stick', params: { sizeFactor: 0.15, aromaticBonds: false } },
-                colorTheme: { name: 'uniform', params: { value: Color(color) } },
-            };
-        };
-
-        const stepDesc = Step.fromName(stepName);
-        if (!stepDesc)
-            return;
-        const stepIdx = this.stepNames.get(stepName);
-        if (stepIdx === undefined) {
-            console.error(`Unknown step name ${stepName}`);
-            return;
-        }
-
-        if (stepDesc.model !== this.currentModelNumber()) {
-            const b = this.getBuilder('model')
-                .update(
-                    StateTransforms.Model.ModelFromTrajectory,
-                    old => ({
-                        ...old,
-                        modelIndex: stepDesc.model - 1,
-                    })
-                );
-            await b.commit();
-        }
-
-        const entireStru = this.plugin.state.data.cells.get(IDs.ID('structure', 'nucleic', BaseRef))!.obj!;
-        const loci = Traverse.findResidue(
-            stepDesc.chain,
-            stepDesc.resNo1,
-            stepDesc.altId1,
-            StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(entireStru.data, entireStru.data)),
-            'auth'
+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>
         );
-        if (loci.kind !== 'element-loci')
-            return;
-        const selLoci = Loci.normalize(loci, 'two-residues');
-        if (selLoci.kind !== 'element-loci')
-            return;
-
-        // HACK: This is a temporary hack until we get prev-next steps mapping into the plugin
-        let stepPrev = undefined;
-        if (prevStepName) {
-            const idx = this.stepNames.get(prevStepName);
-            stepPrev = (idx !== undefined) ? this.steps[idx] : undefined;
-        }
-        let stepNext = undefined;
-        if (nextStepName) {
-            const idx = this.stepNames.get(nextStepName);
-            stepNext = (idx !== undefined) ? this.steps[idx] : undefined;
-        }
-
-        const step = this.steps[stepIdx];
-
-        const ntcRefSel = step ? this.ntcRef(step, 'sel') : void 0;
-        const ntcRefPrev = stepPrev ? this.ntcRef(stepPrev, 'prev') : void 0;
-        const ntcRefNext = stepNext ? this.ntcRef(stepNext, 'next') : void 0;
-
-        if (!ntcRefSel) {
-            console.error(`stepIdx ${stepIdx} does not map to a known step`);
-            return;
-        }
-
-        const b = this.plugin.state.data.build()
-            .delete(IDs.ID('superposition', '', NtCSupSel))
-            .delete(IDs.ID('superposition', '', NtCSupPrev))
-            .delete(IDs.ID('superposition', '', NtCSupNext));
-
-        const addReference = (ntcRef: string, superposRef: string, loci: Loci, altId1: string|undefined, altId2: string|undefined, color: number) => {
-            const refStru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRef))!.obj!;
-            const refLoci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(refStru.data, refStru.data));
-
-            if (loci.kind === 'element-loci' && Step.is(loci)) {
-                const { bTransform, rmsd } = this.superpose(refLoci, loci, altId1, altId2);
-                if (isNaN(bTransform[0])) {
-                    console.error(`Cannot superpose reference conformer ${ntcRef} onto selection`);
-                    return;
-                }
-                b.to(IDs.ID('structure', '', ntcRef))
-                    .apply(
-                        StateTransforms.Model.TransformStructureConformation,
-                        { transform: { name: 'matrix', params: { data: bTransform, transpose: false } } },
-                        { ref: IDs.ID('superposition', '', superposRef) }
-                    ).apply(
-                        StateTransforms.Representation.StructureRepresentation3D,
-                        ReferenceVisuals(color),
-                        { ref: IDs.ID('visual', '', superposRef) }
-                    );
-                return rmsd;
-            }
-        };
-
-        const rmsd = addReference(ntcRefSel, NtCSupSel, selLoci, stepDesc.altId1, stepDesc.altId2, 0x008000);
-        if (ntcRefPrev) {
-            const { altId1, altId2 } = stepPrev!;
-            addReference(ntcRefPrev, NtCSupPrev, Loci.normalize(Traverse.residue(-1, altId1, selLoci), 'two-residues'), altId1, altId2, 0x0000FF);
-        }
-        if (ntcRefNext) {
-            const { altId1, altId2 } = stepNext!;
-            addReference(ntcRefNext, NtCSupNext, Loci.normalize(Traverse.residue(1, altId1, selLoci), 'two-residues'), altId1, altId2, 0x00FFFF);
-        }
-
-        await b.commit();
-
-        return rmsd;
-    }
-
-    async toggleSubstructure(sub: IDs.Substructure, display: Partial<Display>) {
-        const show = sub === 'nucleic' ? !!display.showNucleic :
-            sub === 'protein' ? !!display.showProtein : !!display.showWater;
-        const repr = display.representation ?? 'cartoon';
-
-        if (show) {
-            const b = this.getBuilder('structure', sub);
-            const visuals = this.substructureVisuals(sub === 'water' ? 'ball-and-stick' : repr);
-            if (b) {
-                b.apply(
-                    StateTransforms.Representation.StructureRepresentation3D,
-                    visuals,
-                    { ref: IDs.ID('visual', sub, BaseRef) }
-                );
-                await b.commit();
-            }
-        } else {
-            await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) });
-            this.resetCameraRadius();
-        }
     }
 }
 
@@ -1030,9 +68,19 @@ interface State {
     display: Display;
     showControls: boolean;
 }
-class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
+export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
     private presentConformers: string[] = [];
-    private viewer: ReDNATCOMspViewer|null = null;
+    private viewer: ReDNATCOMspViewer|undefined = undefined;
+    private selectedStep: { name: string, rmsd?: number }|undefined;
+
+    constructor(props: ReDNATCOMsp.Props) {
+        super(props);
+
+        this.state = {
+            display: { ...Display },
+            showControls: false,
+        };
+    }
 
     private classColorToConformers(k: keyof ConformersByClass, color: Color) {
         const updated: Partial<NtCColors.Conformers> = {};
@@ -1074,15 +122,36 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
         this.setState({ ...this.state, display });
     }
 
-    async command(cmd: Commands.Cmd) {
+    apiQuery(type: Api.Queries.Type): Api.Response {
+        if (type === 'selected-step') {
+            if (this.selectedStep)
+                return Api.Queries.SelectedStep(this.selectedStep.name, this.selectedStep.rmsd);
+            return Api.Queries.SelectedStep('');
+        }
+
+        // TODO: This cannot happen - figure out how to assert on this
+        return Api.Queries.SelectedStep('');
+    }
+
+    async command(cmd: Api.Command) {
         if (!this.viewer)
             return;
 
         if (cmd.type === 'redraw')
             window.dispatchEvent(new Event('resize'));
-        else if (cmd.type === 'select-step') {
-            await this.viewer.superposeReferences(cmd.stepName, cmd.prevStepName, cmd.nextStepName, cmd.referenceNtC, cmd.references);
+        else if (cmd.type === 'deselect-step') {
+            await this.viewer.actionDeselectStep(this.state.display);
+        } else if (cmd.type === 'select-step') {
+            const ret = await this.viewer.actionSelectStep(cmd.stepName, cmd.prevStepName, cmd.nextStepName, cmd.referenceNtC, cmd.references, this.state.display);
+            if (!ret) {
+                ReDNATCOMspApi.event(Api.Events.StepSelectedFail());
+                return;
+            }
+
             this.viewer.focusOnSelectedStep();
+            this.selectedStep = { name: cmd.stepName, rmsd: ret.rmsd };
+
+            ReDNATCOMspApi.event(Api.Events.StepSelectedOk(this.selectedStep.name, this.selectedStep.rmsd));
         } else if (cmd.type === 'switch-model') {
             if (cmd.model < 1 || cmd.model > this.viewer.getModelCount())
                 return;
@@ -1097,15 +166,6 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
         }
     }
 
-    constructor(props: ReDNATCOMsp.Props) {
-        super(props);
-
-        this.state = {
-            display: { ...Display },
-            showControls: false,
-        };
-    }
-
     loadStructure(data: string, type: 'pdb'|'cif') {
         if (this.viewer)
             this.viewer.loadStructure(data, type, this.state.display).then(() => {
@@ -1114,10 +174,19 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
             });
     }
 
+    viewerStepDeselected() {
+        this.viewer!.actionDeselectStep(this.state.display);
+        ReDNATCOMspApi.event(Api.Events.StepDeselected());
+    }
+
+    viewerStepSelected(stepName: string) {
+        ReDNATCOMspApi.event(Api.Events.StepRequested(stepName));
+    }
+
     componentDidMount() {
         if (!this.viewer) {
             const elem = document.getElementById(this.props.elemId + '-viewer');
-            ReDNATCOMspViewer.create(elem!).then(viewer => {
+            ReDNATCOMspViewer.create(elem!, this).then(viewer => {
                 this.viewer = viewer;
                 this.viewer.loadReferenceConformers().then(() => {
                     ReDNATCOMspApi._bind(this);
@@ -1341,7 +410,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
     }
 }
 
-namespace ReDNATCOMsp {
+export namespace ReDNATCOMsp {
     export interface Props {
         elemId: string;
         onInited?: () => void;
@@ -1356,32 +425,4 @@ namespace ReDNATCOMsp {
     }
 }
 
-class _ReDNATCOMspApi {
-    private target: ReDNATCOMsp|null = null;
-
-    private check() {
-        if (!this.target)
-            throw new Error('ReDNATCOMsp object not bound');
-    }
-
-    _bind(target: ReDNATCOMsp) {
-        this.target = target;
-    }
-
-    command(cmd: Commands.Cmd) {
-        this.check();
-        this.target!.command(cmd);
-    }
-
-    init(elemId: string, onInited?: () => void) {
-        ReDNATCOMsp.init(elemId, onInited);
-        return this;
-    }
-
-    loadStructure(data: string) {
-        this.check();
-        this.target!.loadStructure(data, 'cif');
-    }
-}
-
-export const ReDNATCOMspApi = new _ReDNATCOMspApi();
+export const ReDNATCOMspApi = new ReDNATCOMspApiImpl();
diff --git a/src/apps/rednatco/traverse.ts b/src/apps/rednatco/traverse.ts
index e4eafbfcdc3927acf55e1d4e13ce548c0753eb85..913b679694f8c8affc7d5a751c056ca9e0aa3eb3 100644
--- a/src/apps/rednatco/traverse.ts
+++ b/src/apps/rednatco/traverse.ts
@@ -66,6 +66,14 @@ export namespace Traverse {
         return EmptyLoci;
     }
 
+    export function findStep(asymId: string, seqId: number, altId: string|undefined, loci: StructureElement.Loci, source: 'label'|'auth') {
+        const sel = findResidue(asymId, seqId, altId, loci, source);
+        if (sel.kind === 'empty-loci')
+            return sel;
+
+        return Loci.normalize(sel, 'two-residues');
+    }
+
     export function residue(shift: number, altId: string|undefined, cursor: StructureElement.Loci) {
         for (const e of cursor.elements) {
             const entireUnit = cursor.structure.units[e.unit.id];
diff --git a/src/apps/rednatco/viewer.ts b/src/apps/rednatco/viewer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..01d5e8f06506008c69574dd54b427eb40823044b
--- /dev/null
+++ b/src/apps/rednatco/viewer.ts
@@ -0,0 +1,1084 @@
+import * as IDs from './idents';
+import * as RefCfmr from './reference-conformers';
+import { ReDNATCOMsp, Display, VisualRepresentations } from './index';
+import { NtCColors } from './colors';
+import { ReferenceConformersPdbs } from './reference-conformers-pdbs';
+import { Step } from './step';
+import { Superpose } from './superpose';
+import { Traverse } from './traverse';
+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 { 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 { structureUnion, structureSubtract } from '../../mol-model/structure/query/utils/structure-set';
+import { Location } from '../../mol-model/structure/structure/element/location';
+import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { PluginBehavior, PluginBehaviors } from '../../mol-plugin/behavior';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginContext } from '../../mol-plugin/context';
+import { PluginSpec } from '../../mol-plugin/spec';
+import { LociLabel } from '../../mol-plugin-state/manager/loci-label';
+import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { RawData } from '../../mol-plugin-state/transforms/data';
+import { createPluginUI } from '../../mol-plugin-ui';
+import { PluginUIContext } from '../../mol-plugin-ui/context';
+import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
+import { Representation } from '../../mol-repr/representation';
+import { StateObjectCell, StateObject, StateSelection, StateTransformer } from '../../mol-state';
+import { StateBuilder } from '../../mol-state/state/builder';
+import { StateTreeSpine } from '../../mol-state/tree/spine';
+import { lociLabel } from '../../mol-theme/label';
+import { arrayMax } from '../../mol-util/array';
+import { Binding } from '../../mol-util/binding';
+import { Color } from '../../mol-util/color';
+import { ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
+import { MarkerAction } from '../../mol-util/marker-action';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ObjectKeys } from '../../mol-util/type-helpers';
+import './molstar.css';
+import './rednatco-molstar.css';
+
+const Extensions = {
+    'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
+};
+
+const AnimationDurationMsec = 150;
+const BaseRef = 'rdo';
+const RCRef = 'rc';
+const NtCSupPrev = 'ntc-sup-prev';
+const NtCSupSel = 'ntc-sup-sel';
+const NtCSupNext = 'ntc-sup-next';
+const SphereBoundaryHelper = new BoundaryHelper('98');
+
+const DefaultChainColor = Color(0xD9D9D9);
+
+type StepInfo = {
+    name: string;
+    assignedNtC: string;
+    closestNtC: string; // Fallback for cases where assignedNtC is NANT
+    chain: string;
+    resNo1: number;
+    resNo2: number;
+    altId1?: string;
+    altId2?: string;
+    model: number;
+}
+
+type StepWithStructure = {
+    step: StepInfo;
+    loci: StructureElement.Loci;
+}
+
+function dinucleotideBackbone(loci: StructureElement.Loci, altId1?: string, altId2?: string) {
+    const es = loci.elements[0];
+    const loc = Location.create(loci.structure, es.unit, es.unit.elements[OrderedSet.getAt(es.indices, 0)]);
+    const len = OrderedSet.size(es.indices);
+    const indices = new Array<ElementIndex>();
+
+    const gather = (atoms: string[], start: number, end: number, altId?: string) => {
+        for (const atom of atoms) {
+            let idx = start;
+            for (; idx < end; idx++) {
+                loc.element = es.unit.elements[OrderedSet.getAt(es.indices, idx)];
+                const _atom = StructureProperties.atom.label_atom_id(loc);
+                if (atom === _atom) {
+                    if (altId) {
+                        const _altId = StructureProperties.atom.label_alt_id(loc);
+                        if (_altId !== '' && _altId !== altId)
+                            continue;
+                    }
+
+                    indices.push(loc.element);
+                    break;
+                }
+            }
+            if (idx === end) {
+                console.error(`Cannot find backbone atom ${atom} in first residue of a step`);
+                return false;
+            }
+        }
+
+        return true;
+    };
+
+    // Find split between first and second residue
+    const resNo1 = StructureProperties.residue.auth_seq_id(loc);
+    let secondIdx = -1;
+    for (let idx = 0; idx < len; idx++) {
+        loc.element = es.unit.elements[OrderedSet.getAt(es.indices, idx)];
+        const resNo = StructureProperties.residue.auth_seq_id(loc);
+        if (resNo !== resNo1) {
+            secondIdx = idx;
+            break;
+        }
+    }
+    if (secondIdx === -1)
+        return [];
+
+    // Gather ElementIndices for backbone atoms of the first  residue
+    loc.element = es.unit.elements[OrderedSet.getAt(es.indices, 0)];
+    const ring1 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
+    if (!ring1)
+        return [];
+
+    const first = RefCfmr.BackboneAtoms.first.concat(RefCfmr.BackboneAtoms[ring1]);
+    if (!gather(first, 0, secondIdx, altId1))
+        return [];
+
+    loc.element = es.unit.elements[OrderedSet.getAt(es.indices, secondIdx)];
+    const ring2 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
+    if (!ring2)
+        return [];
+
+    const second = RefCfmr.BackboneAtoms.second.concat(RefCfmr.BackboneAtoms[ring2]);
+    if (!gather(second, secondIdx, len, altId2))
+        return [];
+
+    return indices;
+}
+
+function rcref(c: string, where: 'sel'|'prev'|'next'|'' = '') {
+    return `${RCRef}-${c}-${where}`;
+}
+
+const ReDNATCOLociLabelProvider = PluginBehavior.create({
+    name: 'watlas-loci-label-provider',
+    category: 'interaction',
+    ctor: class implements PluginBehavior<undefined> {
+        private f = {
+            label: (loci: Loci) => {
+                switch (loci.kind) {
+                    case 'structure-loci':
+                    case 'element-loci':
+                        return lociLabel(loci);
+                    default:
+                        return '';
+                }
+            },
+            group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'),
+            priority: 100
+        };
+        register() { this.ctx.managers.lociLabels.addProvider(this.f); }
+        unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
+        constructor(protected ctx: PluginContext) { }
+    },
+    display: { name: 'ReDNATCO labeling' }
+});
+
+const ReDNATCOLociSelectionBindings = {
+    clickFocus: Binding([Binding.Trigger(ButtonsType.Flag.Secondary)], 'Focus camera on selected loci using ${triggers}'),
+    clickSelectOnly: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Select the clicked element using ${triggers}.'),
+    clickDeselectAllOnEmpty: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Deselect all when clicking on nothing using ${triggers}.'),
+};
+const ReDNATCOLociSelectionParams = {
+    bindings: PD.Value(ReDNATCOLociSelectionBindings, { isHidden: true }),
+    onDeselected: PD.Value(() => {}, { isHidden: true }),
+    onSelected: PD.Value((loci: Representation.Loci) => {}, { isHidden: true }),
+};
+type ReDNATCOLociSelectionProps = PD.Values<typeof ReDNATCOLociSelectionParams>;
+
+const ReDNATCOLociSelectionProvider = PluginBehavior.create({
+    name: 'rednatco-loci-selection-provider',
+    category: 'interaction',
+    display: { name: 'Interactive step selection' },
+    params: () => ReDNATCOLociSelectionParams,
+    ctor: class extends PluginBehavior.Handler<ReDNATCOLociSelectionProps> {
+        private spine: StateTreeSpine.Impl;
+        private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
+            if (!this.ctx.canvas3d) return;
+            this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
+        };
+        private applySelectMark(ref: string, clear?: boolean) {
+            const cell = this.ctx.state.data.cells.get(ref);
+            if (cell && PSO.isRepresentation3D(cell.obj)) {
+                this.spine.current = cell;
+                const so = this.spine.getRootOfType(PSO.Molecule.Structure);
+                if (so) {
+                    if (clear) {
+                        this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
+                    }
+                    const loci = this.ctx.managers.structure.selection.getLoci(so.data);
+                    this.lociMarkProvider({ loci }, MarkerAction.Select);
+                }
+            }
+        }
+        private focusOnLoci(loci: Representation.Loci) {
+            if (!this.ctx.canvas3d)
+                return;
+
+            const sphere = Loci.getBoundingSphere(loci.loci);
+            if (!sphere)
+                return;
+            const snapshot = this.ctx.canvas3d.camera.getSnapshot();
+            snapshot.target = sphere.center;
+
+            PluginCommands.Camera.SetSnapshot(this.ctx, { snapshot, durationMs: AnimationDurationMsec });
+        }
+        register() {
+            const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci);
+            const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci);
+
+            const actions: [keyof typeof ReDNATCOLociSelectionBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
+                ['clickFocus', current => this.focusOnLoci(current), lociIsNotEmpty],
+                [
+                    'clickDeselectAllOnEmpty',
+                    () => {
+                        this.ctx.managers.interactivity.lociSelects.deselectAll();
+                        this.params.onDeselected();
+                    },
+                    lociIsEmpty
+                ],
+                [
+                    'clickSelectOnly',
+                    current => {
+                        this.ctx.managers.interactivity.lociSelects.deselectAll();
+                        if (current.loci.kind === 'element-loci') {
+                            this.ctx.managers.interactivity.lociSelects.select(current);
+                            this.params.onSelected(current);
+                        }
+                    },
+                    lociIsNotEmpty
+                ],
+            ];
+
+            // sort the action so that the ones with more modifiers trigger sooner.
+            actions.sort((a, b) => {
+                const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
+                const k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(t => ModifiersKeys.size(t.modifiers)));
+                const l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(t => ModifiersKeys.size(t.modifiers)));
+                return l - k;
+            });
+
+            this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
+                if (!this.ctx.canvas3d) return;
+
+                // only trigger the 1st action that matches
+                for (const [binding, action, condition] of actions) {
+                    if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
+                        action(current);
+                        break;
+                    }
+                }
+            });
+
+            this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
+
+            this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
+
+            // re-apply select-mark to all representation of an updated structure
+            this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, oldData, action }) => {
+                const cell = this.ctx.state.data.cells.get(ref);
+                if (cell && PSO.Molecule.Structure.is(cell.obj)) {
+                    const structure: Structure = obj.data;
+                    const oldStructure: Structure | undefined = action === 'recreate' ? oldObj?.data :
+                        action === 'in-place' ? oldData : undefined;
+                    if (oldStructure &&
+                        Structure.areEquivalent(structure, oldStructure) &&
+                        Structure.areHierarchiesEqual(structure, oldStructure)) return;
+
+                    const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(PSO.Molecule.Structure.Representation3D, ref));
+                    for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);
+                }
+            });
+
+        }
+        unregister() {
+        }
+        constructor(ctx: PluginContext, params: ReDNATCOLociSelectionProps) {
+            super(ctx, params);
+            this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
+        }
+    },
+});
+
+export class ReDNATCOMspViewer {
+    private haveMultipleModels = false;
+    private steps: StepInfo[] = [];
+    private stepNames: Map<string, number> = new Map();
+    private app: ReDNATCOMsp;
+
+    constructor(public plugin: PluginUIContext, interactionContext: { self?: ReDNATCOMspViewer }, app: ReDNATCOMsp) {
+        interactionContext.self = this;
+        this.app = app;
+    }
+
+    private currentModelNumber() {
+        const model = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
+        if (!model)
+            return -1;
+        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));
+    }
+
+    private getStructureParent(cell: StateObjectCell) {
+        if (!cell.sourceRef)
+            return undefined;
+        const parent = this.plugin.state.data.cells.get(cell.sourceRef);
+        if (!parent)
+            return undefined;
+        return parent.obj?.type.name === 'Structure' ? parent.obj : undefined;
+    }
+
+    private ntcRef(step: StepInfo, where: 'sel'|'prev'|'next') {
+        return rcref(step.assignedNtC === 'NANT' ? step.closestNtC : step.assignedNtC, where);
+    }
+
+    private pyramidsParams(colors: NtCColors.Conformers, visible: Map<string, boolean>, transparent: boolean) {
+        const typeParams = {} as PD.Values<ConfalPyramidsParams>;
+        for (const k of Reflect.ownKeys(ConfalPyramidsParams) as (keyof ConfalPyramidsParams)[]) {
+            if (ConfalPyramidsParams[k].type === 'boolean')
+                (typeParams[k] as any) = visible.get(k) ?? ConfalPyramidsParams[k]['defaultValue'];
+        }
+
+        return {
+            type: { name: 'confal-pyramids', params: { ...typeParams, alpha: transparent ? 0.5 : 1.0 } },
+            colorTheme: {
+                name: 'confal-pyramids',
+                params: {
+                    colors: {
+                        name: 'custom',
+                        params: colors,
+                    },
+                },
+            },
+        };
+    }
+
+    private resetCameraRadius() {
+        if (!this.plugin.canvas3d)
+            return;
+
+        const spheres = [];
+        for (const [ref, cell] of Array.from(this.plugin.state.data.cells)) {
+            if (!IDs.isVisual(ref))
+                continue;
+            const parent = this.getStructureParent(cell);
+            if (parent) {
+                const loci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(parent.data, parent.data));
+                const s = Loci.getBoundingSphere(loci);
+                if (s)
+                    spheres.push(s);
+            }
+        }
+
+        if (spheres.length === 0)
+            return;
+
+        SphereBoundaryHelper.reset();
+        for (const s of spheres)
+            SphereBoundaryHelper.includePositionRadius(s.center, s.radius);
+        SphereBoundaryHelper.finishedIncludeStep();
+        for (const s of spheres)
+            SphereBoundaryHelper.radiusPositionRadius(s.center, s.radius);
+        const bs = SphereBoundaryHelper.getSphere();
+
+        const snapshot = this.plugin.canvas3d.camera.getSnapshot();
+        snapshot.radius = bs.radius;
+        snapshot.target = bs.center;
+        PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: AnimationDurationMsec });
+    }
+
+    private stepFromName(name: string) {
+        const idx = this.stepNames.get(name);
+        if (idx === undefined)
+            return undefined;
+
+        return this.steps[idx];
+    }
+
+    private substructureVisuals(representation: 'ball-and-stick'|'cartoon') {
+        switch (representation) {
+            case 'cartoon':
+                return {
+                    type: {
+                        name: 'cartoon',
+                        params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false },
+                    },
+                    colorTheme: { name: 'uniform', params: { color: DefaultChainColor } }
+                };
+            case 'ball-and-stick':
+                return {
+                    type: {
+                        name: 'ball-and-stick',
+                        params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false },
+                    },
+                    colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'custom', params: DefaultChainColor } } },
+                };
+        }
+    }
+
+    private superpose(reference: StructureElement.Loci, stru: StructureElement.Loci, altId1?: string, altId2?: string) {
+        const refElems = dinucleotideBackbone(reference);
+        const struElems = dinucleotideBackbone(stru, altId1, altId2);
+
+        return Superpose.superposition(
+            { elements: refElems, conformation: reference.elements[0].unit.conformation },
+            { elements: struElems, conformation: stru.elements[0].unit.conformation }
+        );
+    }
+
+    private async toggleNucleicSubstructure(show: boolean, repr: VisualRepresentations) {
+        if (this.has('structure', 'remainder-slice', BaseRef)) {
+            const b = this.getBuilder('structure', 'remainder-slice');
+            if (show) {
+                b.apply(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    this.substructureVisuals(repr),
+                    { ref: IDs.ID('visual', 'remainder-slice', BaseRef) }
+                );
+            } else
+                b.delete(IDs.ID('visual', 'remainder-slice', BaseRef));
+
+            await b.commit();
+        } else {
+            const b = this.getBuilder('structure', 'nucleic');
+
+            if (show) {
+                b.apply(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    this.substructureVisuals(repr),
+                    { ref: IDs.ID('visual', 'nucleic', BaseRef) }
+                );
+            } else
+                b.delete(IDs.ID('visual', 'nucleic', BaseRef));
+
+            await b.commit();
+        }
+    }
+
+    private toStepWithStructure(name: string, struLoci: StructureElement.Loci): StepWithStructure|undefined {
+        const step = this.stepFromName(name);
+        if (!step)
+            return void 0;
+
+        const loci = Traverse.findStep(
+            step.chain,
+            step.resNo1,
+            step.altId1,
+            struLoci,
+            'auth'
+        );
+        if (loci.kind === 'element-loci')
+            return { step, loci };
+
+        return void 0;
+    }
+
+    static async create(target: HTMLElement, app: ReDNATCOMsp) {
+        const interactCtx: { self?: ReDNATCOMspViewer } = { self: undefined };
+        const defaultSpec = DefaultPluginUISpec();
+        const spec: PluginUISpec = {
+            ...defaultSpec,
+            behaviors: [
+                PluginSpec.Behavior(ReDNATCOLociLabelProvider),
+                PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
+                PluginSpec.Behavior(
+                    ReDNATCOLociSelectionProvider,
+                    {
+                        bindings: ReDNATCOLociSelectionBindings,
+                        onDeselected: () => interactCtx.self!.onDeselected(),
+                        onSelected: (loci) => interactCtx.self!.onLociSelected(loci),
+                    }
+                ),
+                ...ObjectKeys(Extensions).map(k => Extensions[k]),
+            ],
+            components: {
+                ...defaultSpec.components,
+                controls: {
+                    ...defaultSpec.components?.controls,
+                    top: 'none',
+                    right: 'none',
+                    bottom: 'none',
+                    left: 'none'
+                },
+            },
+            layout: {
+                initial: {
+                    isExpanded: false,
+                    showControls: false,
+                },
+            },
+        };
+
+        const plugin = await createPluginUI(target, spec);
+
+        plugin.managers.interactivity.setProps({ granularity: 'two-residues' });
+        plugin.selectionMode = true;
+
+        return new ReDNATCOMspViewer(plugin, interactCtx, app);
+    }
+
+    async changeNtCColors(display: Partial<Display>) {
+        if (!this.has('pyramids', 'nucleic'))
+            return;
+
+        const b = this.plugin.state.data.build().to(IDs.ID('pyramids', 'nucleic', BaseRef));
+        b.update(
+            StateTransforms.Representation.StructureRepresentation3D,
+            old => ({
+                ...old,
+                colorTheme: {
+                    name: 'confal-pyramids',
+                    params: {
+                        colors: {
+                            name: 'custom',
+                            params: display.conformerColors ?? NtCColors.Conformers,
+                        },
+                    },
+                },
+            })
+        );
+
+        await b.commit();
+    }
+
+    async changePyramids(display: Partial<Display>) {
+        if (display.showPyramids) {
+            if (!this.has('pyramids', 'nucleic')) {
+                const b = this.getBuilder('structure', 'nucleic');
+                if (b) {
+                    b.apply(
+                        StateTransforms.Representation.StructureRepresentation3D,
+                        this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false),
+                        { ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
+                    );
+                    await b.commit();
+                }
+            } else {
+                const b = this.getBuilder('pyramids', 'nucleic');
+                b.update(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    old => ({
+                        ...old,
+                        ...this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false),
+                    })
+                );
+                await b.commit();
+            }
+        } else
+            await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('pyramids', 'nucleic', BaseRef) });
+    }
+
+    async changeRepresentation(display: Partial<Display>) {
+        const b = this.plugin.state.data.build();
+        const repr = display.representation ?? 'cartoon';
+
+        for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) {
+            if (this.has('visual', sub)) {
+                b.to(IDs.ID('visual', sub, BaseRef))
+                    .update(
+                        StateTransforms.Representation.StructureRepresentation3D,
+                        old => ({
+                            ...old,
+                            ...this.substructureVisuals(repr),
+                        })
+                    );
+            }
+        }
+
+        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)
+            return void 0;
+        const model = (obj as StateObject<Model>);
+        const sourceData = model.data.sourceData;
+        if (!MmcifFormat.is(sourceData))
+            return void 0;
+
+        const tableSum = sourceData.data.frame.categories['ndb_struct_ntc_step_summary'];
+        const tableStep = sourceData.data.frame.categories['ndb_struct_ntc_step'];
+        if (!tableSum || !tableStep) {
+            console.warn('NtC information not present');
+            return void 0;
+        }
+
+        const _ids = tableStep.getField('id');
+        const _names = tableStep.getField('name');
+        const _chains = tableStep.getField('auth_asym_id_1');
+        const _authSeqId1 = tableStep.getField('auth_seq_id_1');
+        const _authSeqId2 = tableStep.getField('auth_seq_id_2');
+        const _labelAltId1 = tableStep.getField('label_alt_id_1');
+        const _labelAltId2 = tableStep.getField('label_alt_id_2');
+        const _stepIds = tableSum.getField('step_id');
+        const _assignedNtCs = tableSum.getField('assigned_NtC');
+        const _closestNtCs = tableSum.getField('closest_NtC');
+        const _models = tableStep.getField('PDB_model_number');
+        if (!_ids || !_names || !_chains || !_stepIds || !_assignedNtCs || !_closestNtCs || !_labelAltId1 || !_labelAltId2 || !_authSeqId1 || !_authSeqId2 || !_models) {
+            console.warn('Expected fields are not present in NtC categories');
+            return void 0;
+        }
+
+        const ids = _ids.toIntArray();
+        const names = _names.toStringArray();
+        const chains = _chains.toStringArray();
+        const authSeqId1 = _authSeqId1.toIntArray();
+        const authSeqId2 = _authSeqId2.toIntArray();
+        const labelAltId1 = _labelAltId1.toStringArray();
+        const labelAltId2 = _labelAltId2.toStringArray();
+        const stepIds = _stepIds.toIntArray();
+        const assignedNtCs = _assignedNtCs.toStringArray();
+        const closestNtCs = _closestNtCs.toStringArray();
+        const models = _models.toIntArray();
+        const len = ids.length;
+
+        const stepNames = new Map<string, number>();
+        const steps = new Array<StepInfo>(len);
+
+        for (let idx = 0; idx < len; idx++) {
+            const id = ids[idx];
+            const name = names[idx];
+            for (let jdx = 0; jdx < len; jdx++) {
+                if (stepIds[jdx] === id) {
+                    const assignedNtC = assignedNtCs[jdx];
+                    const closestNtC = closestNtCs[jdx];
+                    const chain = chains[jdx];
+                    const resNo1 = authSeqId1[jdx];
+                    const resNo2 = authSeqId2[jdx];
+                    const altId1 = labelAltId1[jdx] === '' ? void 0 : labelAltId1[jdx];
+                    const altId2 = labelAltId2[jdx] === '' ? void 0 : labelAltId2[jdx];
+                    const model = models[jdx];
+
+                    // We're assuming that steps are ID'd with a contigious, monotonic sequence starting from 1
+                    steps[id - 1] = {
+                        name,
+                        assignedNtC,
+                        closestNtC,
+                        chain,
+                        resNo1,
+                        resNo2,
+                        altId1,
+                        altId2,
+                        model
+                    };
+                    stepNames.set(name, id - 1);
+                    break;
+                }
+            }
+        }
+
+        return { steps, stepNames };
+    }
+
+    getModelCount() {
+        const obj = this.plugin.state.data.cells.get(IDs.ID('trajectory', '', BaseRef))?.obj;
+        if (!obj)
+            return 0;
+        return (obj as StateObject<Trajectory>).data.frameCount;
+    }
+
+    getPresentConformers() {
+        const obj = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
+        if (!obj)
+            return [];
+        const model = (obj as StateObject<Model>);
+        const modelNum = model.data.modelNum;
+        const sourceData = model.data.sourceData;
+        if (MmcifFormat.is(sourceData)) {
+            const tableSum = sourceData.data.frame.categories['ndb_struct_ntc_step_summary'];
+            const tableStep = sourceData.data.frame.categories['ndb_struct_ntc_step'];
+            if (!tableSum || !tableStep) {
+                console.warn('NtC information not present');
+                return [];
+            }
+
+            const _stepIds = tableSum.getField('step_id');
+            const _assignedNtCs = tableSum.getField('assigned_NtC');
+            const _ids = tableStep.getField('id');
+            const _modelNos = tableStep.getField('PDB_model_number');
+            if (!_stepIds || !_assignedNtCs || !_ids || !_modelNos) {
+                console.warn('Expected fields are not present in NtC categories');
+                return [];
+            }
+
+            const stepIds = _stepIds.toIntArray();
+            const assignedNtCs = _assignedNtCs.toStringArray();
+            const ids = _ids.toIntArray();
+            const modelNos = _modelNos.toIntArray();
+
+            const present = new Array<string>();
+            for (let row = 0; row < stepIds.length; row++) {
+                const idx = ids.indexOf(stepIds[row]);
+                if (modelNos[idx] === modelNum) {
+                    const ntc = assignedNtCs[row];
+                    if (!present.includes(ntc))
+                        present.push(ntc);
+                }
+            }
+
+            present.sort();
+            return present;
+        }
+        return [];
+    }
+
+    has(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
+        return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj?.data;
+    }
+
+    isReady() {
+        return this.has('structure', '', BaseRef);
+    }
+
+    async loadStructure(data: string, type: 'pdb'|'cif', display: Partial<Display>) {
+        // TODO: Remove the currently loaded structure
+
+        const b = (t => type === 'pdb'
+            ? t.apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
+            : t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
+        )(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', '', BaseRef) }))
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 }, { ref: IDs.ID('model', '', BaseRef) })
+            .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', BaseRef) })
+            // Extract substructures
+            .apply(StateTransforms.Model.StructureComplexElement, { type: 'nucleic' }, { ref: IDs.ID('structure', 'nucleic', BaseRef) })
+            .to(IDs.ID('structure', '', BaseRef))
+            .apply(StateTransforms.Model.StructureComplexElement, { type: 'protein' }, { ref: IDs.ID('structure', 'protein', BaseRef) })
+            .to(IDs.ID('structure', '', BaseRef))
+            .apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: IDs.ID('structure', 'water', BaseRef) });
+        // Commit now so that we can check whether individual substructures are available
+        await b.commit();
+
+        // Create default visuals
+        const bb = this.plugin.state.data.build();
+        if (display.showNucleic && this.has('structure', 'nucleic')) {
+            bb.to(IDs.ID('structure', 'nucleic', BaseRef))
+                .apply(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    this.substructureVisuals('cartoon'),
+                    { ref: IDs.ID('visual', 'nucleic', BaseRef) }
+                );
+            if (display.showPyramids) {
+                bb.to(IDs.ID('structure', 'nucleic', BaseRef))
+                    .apply(
+                        StateTransforms.Representation.StructureRepresentation3D,
+                        this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), false),
+                        { ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
+                    );
+            }
+        }
+        if (display.showProtein && this.has('structure', 'protein')) {
+            bb.to(IDs.ID('structure', 'protein', BaseRef))
+                .apply(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    this.substructureVisuals('cartoon'),
+                    { ref: IDs.ID('visual', 'protein', BaseRef) }
+                );
+        }
+        if (display.showWater && this.has('structure', 'water')) {
+            bb.to(IDs.ID('structure', 'water', BaseRef))
+                .apply(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    this.substructureVisuals('ball-and-stick'),
+                    { ref: IDs.ID('visual', 'water', BaseRef) }
+                );
+        }
+
+        await bb.commit();
+
+        this.haveMultipleModels = this.getModelCount() > 1;
+
+        const ntcInfo = this.gatherStepInfo();
+        if (!ntcInfo) {
+            this.steps.length = 0;
+            this.stepNames.clear();
+        } else {
+            this.steps = ntcInfo.steps;
+            this.stepNames = ntcInfo.stepNames;
+        }
+    }
+
+    async loadReferenceConformers() {
+        const b = this.plugin.state.data.build().toRoot();
+
+        for (const c in ReferenceConformersPdbs) {
+            const cfmr = ReferenceConformersPdbs[c as keyof typeof ReferenceConformersPdbs];
+            const bRef = rcref(c);
+            const mRef = IDs.ID('model', '', bRef);
+            b.toRoot();
+            b.apply(RawData, { data: cfmr, label: `Reference ${c}` }, { ref: IDs.ID('data', '', bRef) })
+                .apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', bRef) })
+                .apply(StateTransforms.Model.ModelFromTrajectory, {}, { ref: mRef })
+                .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'sel')) })
+                .to(mRef)
+                .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'prev')) })
+                .to(mRef)
+                .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'next')) });
+        }
+
+        await b.commit();
+    }
+
+    async onDeselected() {
+        this.app.viewerStepDeselected();
+    }
+
+    async onLociSelected(selected: Representation.Loci) {
+        const loci = Loci.normalize(selected.loci, 'two-residues');
+
+        if (loci.kind === 'element-loci') {
+            const stepDesc = Step.describe(loci);
+            if (stepDesc) {
+                const stepName = Step.name(stepDesc, this.haveMultipleModels);
+                this.app.viewerStepSelected(stepName);
+            }
+        }
+    }
+
+    async actionDeselectStep(display: Partial<Display>) {
+        await this.plugin.state.data.build()
+            .delete(IDs.ID('superposition', '', NtCSupSel))
+            .delete(IDs.ID('superposition', '', NtCSupPrev))
+            .delete(IDs.ID('superposition', '', NtCSupNext))
+            .delete(IDs.ID('structure', 'selected-slice', BaseRef))
+            .delete(IDs.ID('structure', 'remainder-slice', BaseRef))
+            .commit();
+
+        await this.toggleSubstructure('nucleic', { showNucleic: !!display.showNucleic });
+
+        this.resetCameraRadius();
+    }
+
+    async actionSelectStep(stepName: string, stepNamePrev: string|undefined, stepNameNext: string|undefined, referenceNtc: string, references: ('sel'|'prev'|'next')[], display: Partial<Display>): Promise<{ rmsd: number }|undefined> {
+        const stepCurrent = this.stepFromName(stepName);
+        if (!stepCurrent)
+            return void 0;
+
+        // Switch to a different model if the selected step is from a different model
+        // This is the first thing we need to do
+        if (stepCurrent.model !== this.currentModelNumber())
+            await this.switchModel({ modelNumber: stepCurrent.model });
+
+        const entireStruCell = this.plugin.state.data.cells.get(IDs.ID('structure', 'nucleic', BaseRef));
+        if (!entireStruCell)
+            return void 0;
+        const stru = entireStruCell.obj!.data!;
+        const struLoci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(stru, stru));
+
+        const lociCurrent = Traverse.findStep(
+            stepCurrent.chain,
+            stepCurrent.resNo1,
+            stepCurrent.altId1,
+            struLoci,
+            'auth'
+        );
+        if (lociCurrent.kind !== 'element-loci')
+            return void 0;
+
+        const current = {
+            step: stepCurrent,
+            loci: lociCurrent,
+        };
+
+        const prev = stepNamePrev ? this.toStepWithStructure(stepNamePrev, struLoci) : undefined;
+        const next = stepNameNext ? this.toStepWithStructure(stepNameNext, struLoci) : undefined;
+
+        const toUnionize = [StructureElement.Loci.toStructure(current.loci)];
+        if (prev)
+            toUnionize.push(StructureElement.Loci.toStructure(prev.loci));
+        if (next)
+            toUnionize.push(StructureElement.Loci.toStructure(next.loci));
+
+        const slice = structureUnion(stru, toUnionize);
+        const stepBundle = StructureElement.Bundle.fromSubStructure(stru, slice);
+
+        const subtracted = structureSubtract(stru, slice);
+        const remainderBundle = StructureElement.Bundle.fromSubStructure(stru, subtracted);
+
+        const b = this.plugin.state.data.build();
+        b.to(entireStruCell)
+            .apply(
+                StateTransforms.Model.StructureSelectionFromBundle,
+                { bundle: stepBundle, label: 'Step' },
+                { ref: IDs.ID('structure', 'selected-slice', BaseRef) }
+            )
+            .apply(
+                StateTransforms.Representation.StructureRepresentation3D,
+                this.substructureVisuals('ball-and-stick'),
+                { ref: IDs.ID('visual', 'selected-slice', BaseRef) }
+            )
+            .to(entireStruCell)
+            .apply(
+                StateTransforms.Model.StructureSelectionFromBundle,
+                { bundle: remainderBundle, label: 'Remainder' },
+                { ref: IDs.ID('structure', 'remainder-slice', BaseRef) }
+            );
+
+        // Only show the remainder if the nucleic substructure is shown
+        if (display.showNucleic) {
+            b.to(IDs.ID('structure', 'remainder-slice', BaseRef))
+                .apply(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    this.substructureVisuals('cartoon'),
+                    { ref: IDs.ID('visual', 'remainder-slice', BaseRef) }
+                )
+                .delete(IDs.ID('visual', 'nucleic', BaseRef));
+        }
+
+        const rmsd = this.superposeReferences(b.toRoot(), current, prev, next, referenceNtc, references);
+        if (!rmsd)
+            return void 0;
+
+        await b.commit();
+
+        return { rmsd };
+    }
+
+    async switchModel(display: Partial<Display>) {
+        if (display.modelNumber && display.modelNumber === this.currentModelNumber())
+            return;
+
+        const b = this.plugin.state.data.build()
+            .delete(IDs.ID('superposition', '', NtCSupSel))
+            .delete(IDs.ID('superposition', '', NtCSupPrev))
+            .delete(IDs.ID('superposition', '', NtCSupNext))
+            .to(IDs.ID('model', '', BaseRef))
+            .update(
+                StateTransforms.Model.ModelFromTrajectory,
+                old => ({
+                    ...old,
+                    modelIndex: display.modelNumber ? display.modelNumber - 1 : 0
+                })
+            );
+
+        await b.commit();
+    }
+
+    superposeReferences<A extends StateObject, T extends StateTransformer>(b: StateBuilder.To<A, T>, current: StepWithStructure, prev: StepWithStructure|undefined, next: StepWithStructure|undefined, referenceNtc: string, references: ('sel'|'prev'|'next')[]) {
+        const ReferenceVisuals = (color: number) => {
+            return {
+                type: { name: 'ball-and-stick', params: { sizeFactor: 0.15, aromaticBonds: false } },
+                colorTheme: { name: 'uniform', params: { value: Color(color) } },
+            };
+        };
+
+        const ntcRefSel = this.ntcRef(current.step, 'sel')!;
+        const ntcRefPrev = prev ? this.ntcRef(prev.step, 'prev') : undefined;
+        const ntcRefNext = next ? this.ntcRef(next?.step, 'next') : undefined;
+
+        b.delete(IDs.ID('superposition', '', NtCSupSel))
+            .delete(IDs.ID('superposition', '', NtCSupPrev))
+            .delete(IDs.ID('superposition', '', NtCSupNext));
+
+        const addReference = (ntcRef: string, superposRef: string, loci: StructureElement.Loci, altId1: string|undefined, altId2: string|undefined, color: number) => {
+            const refStru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRef))!.obj!;
+            const refLoci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(refStru.data, refStru.data));
+
+            if (Step.is(loci)) {
+                const { bTransform, rmsd } = this.superpose(refLoci, loci, altId1, altId2);
+                if (isNaN(bTransform[0])) {
+                    console.error(`Cannot superpose reference conformer ${ntcRef} onto selection`);
+                    return void 0;
+                }
+                b.to(IDs.ID('structure', '', ntcRef))
+                    .apply(
+                        StateTransforms.Model.TransformStructureConformation,
+                        { transform: { name: 'matrix', params: { data: bTransform, transpose: false } } },
+                        { ref: IDs.ID('superposition', '', superposRef) }
+                    ).apply(
+                        StateTransforms.Representation.StructureRepresentation3D,
+                        ReferenceVisuals(color),
+                        { ref: IDs.ID('visual', '', superposRef) }
+                    );
+                return rmsd;
+            }
+        };
+
+        const rmsd = addReference(ntcRefSel, NtCSupSel, current.loci, current.step.altId1, current.step.altId2, 0x008000);
+        if (ntcRefPrev) {
+            const { altId1, altId2 } = prev!.step;
+            addReference(ntcRefPrev, NtCSupPrev, prev!.loci, altId1, altId2, 0x0000FF);
+        }
+        if (ntcRefNext) {
+            const { altId1, altId2 } = next!.step;
+            addReference(ntcRefNext, NtCSupNext, next!.loci, altId1, altId2, 0x00FFFF);
+        }
+
+        return rmsd;
+    }
+
+    async toggleSubstructure(sub: IDs.Substructure, display: Partial<Display>) {
+        const show = sub === 'nucleic' ? !!display.showNucleic :
+            sub === 'protein' ? !!display.showProtein : !!display.showWater;
+        const repr = display.representation ?? 'cartoon';
+
+        if (sub === 'nucleic')
+            this.toggleNucleicSubstructure(show, repr);
+        else {
+            if (show) {
+                const b = this.getBuilder('structure', sub);
+                const visuals = this.substructureVisuals(sub === 'water' ? 'ball-and-stick' : repr);
+                if (b) {
+                    b.apply(
+                        StateTransforms.Representation.StructureRepresentation3D,
+                        visuals,
+                        { ref: IDs.ID('visual', sub, BaseRef) }
+                    );
+                    await b.commit();
+                }
+            } else {
+                await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) });
+                this.resetCameraRadius();
+            }
+        }
+    }
+}