Skip to content
Snippets Groups Projects
index.tsx 60.5 KiB
Newer Older
import React from 'react';
import ReactDOM from 'react-dom';
Michal Malý's avatar
Michal Malý committed
import { NtCColors } from './colors';
import { ColorPicker } from './color-picker';
Michal Malý's avatar
Michal Malý committed
import { Commands } from './commands';
Michal Malý's avatar
Michal Malý committed
import { PushButton, ToggleButton } from './controls';
import * as IDs from './idents';
Michal Malý's avatar
Michal Malý committed
import * as RefCfmr from './reference-conformers';
import { ReferenceConformersPdbs } from './reference-conformers-pdbs';
import { Step } from './step';
import { Superpose } from './superpose';
Michal Malý's avatar
Michal Malý committed
import { Traverse } from './traverse';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation';
Michal Malý's avatar
Michal Malý committed
import { OrderedSet } from '../../mol-data/int/ordered-set';
Michal Malý's avatar
Michal Malý committed
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { Loci } from '../../mol-model/loci';
Michal Malý's avatar
Michal Malý committed
import { ElementIndex, Model, Structure, StructureElement, StructureProperties, StructureSelection, Trajectory } from '../../mol-model/structure';
Michal Malý's avatar
Michal Malý committed
import { Location } from '../../mol-model/structure/structure/element/location';
Michal Malý's avatar
Michal Malý committed
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';
Michal Malý's avatar
Michal Malý committed
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';
Michal Malý's avatar
Michal Malý committed
import './molstar.css';
import './rednatco-molstar.css';

const Extensions = {
    'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
};

const AnimationDurationMsec = 150;
Michal Malý's avatar
Michal Malý committed
const BaseRef = 'rdo';
const RCRef = 'rc';
const NtCSupPrev = 'ntc-sup-prev';
const NtCSupSel = 'ntc-sup-sel';
const NtCSupNext = 'ntc-sup-next';
Michal Malý's avatar
Michal Malý committed
const SphereBoundaryHelper = new BoundaryHelper('98');

Michal Malý's avatar
Michal Malý committed
type StepInfo = {
    name: string;
    assignedNtC: string;
    closestNtC: string; // Fallback for cases where assignedNtC is NANT
Michal Malý's avatar
Michal Malý committed
    resNo1: number;
    resNo2: number;
    altId1?: string;
    altId2?: string;
Michal Malý's avatar
Michal Malý committed
function capitalize(s: string) {
    if (s.length === 0)
        return s;
    return s[0].toLocaleUpperCase() + s.slice(1);

}

Michal Malý's avatar
Michal Malý committed
function dinucleotideBackbone(loci: StructureElement.Loci, altId1?: string, altId2?: string) {
Michal Malý's avatar
Michal Malý committed
    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);
Michal Malý's avatar
Michal Malý committed
    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;
    };
Michal Malý's avatar
Michal Malý committed

    // 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)];
Michal Malý's avatar
Michal Malý committed
    const ring1 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
Michal Malý's avatar
Michal Malý committed
    if (!ring1)
        return [];

    const first = RefCfmr.BackboneAtoms.first.concat(RefCfmr.BackboneAtoms[ring1]);
Michal Malý's avatar
Michal Malý committed
    if (!gather(first, 0, secondIdx, altId1))
        return [];
Michal Malý's avatar
Michal Malý committed

    loc.element = es.unit.elements[OrderedSet.getAt(es.indices, secondIdx)];
Michal Malý's avatar
Michal Malý committed
    const ring2 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
Michal Malý's avatar
Michal Malý committed
    if (!ring2)
        return [];

    const second = RefCfmr.BackboneAtoms.second.concat(RefCfmr.BackboneAtoms[ring2]);
Michal Malý's avatar
Michal Malý committed
    if (!gather(second, secondIdx, len, altId2))
        return [];
Michal Malý's avatar
Michal Malý committed

    return indices;
}

function rcref(c: string, where: 'sel'|'prev'|'next'|'' = '') {
    return `${RCRef}-${c}-${where}`;
}

Michal Malý's avatar
Michal Malý committed
class ColorBox extends React.Component<{ caption: string, color: Color }> {
Michal Malý's avatar
Michal Malý committed
    render() {
        return (
Michal Malý's avatar
Michal Malý committed
            <div className='rmsp-color-box'>
                <div className='rmsp-color-box-caption'>{this.props.caption}</div>
                <div
                    className='rmsp-color-box-color'
                    style={{ backgroundColor: Color.toStyle(this.props.color) }}
                />
Michal Malý's avatar
Michal Malý committed
const ConformersByClass = {
    A: ['AA00_Upr', 'AA00_Lwr', 'AA02_Upr', 'AA02_Lwr', 'AA03_Upr', 'AA03_Lwr', 'AA04_Upr', 'AA04_Lwr', 'AA08_Upr', 'AA08_Lwr', 'AA09_Upr', 'AA09_Lwr', 'AA01_Upr', 'AA01_Lwr', 'AA05_Upr', 'AA05_Lwr', 'AA06_Upr', 'AA06_Lwr', 'AA10_Upr', 'AA10_Lwr', 'AA11_Upr', 'AA11_Lwr', 'AA07_Upr', 'AA07_Lwr', 'AA12_Upr', 'AA12_Lwr', 'AA13_Upr', 'AA13_Lwr', 'AB01_Upr', 'AB02_Upr', 'AB03_Upr', 'AB04_Upr', 'AB05_Upr', 'BA01_Lwr', 'BA05_Lwr', 'BA09_Lwr', 'BA08_Lwr', 'BA10_Lwr', 'BA13_Lwr', 'BA16_Lwr', 'BA17_Lwr', 'AAS1_Lwr', 'AB1S_Upr'],
    B: ['AB01_Lwr', 'AB02_Lwr', 'AB03_Lwr', 'AB04_Lwr', 'AB05_Lwr', 'BA09_Upr', 'BA10_Upr', 'BB00_Upr', 'BB00_Lwr', 'BB01_Upr', 'BB01_Lwr', 'BB17_Upr', 'BB17_Lwr', 'BB02_Upr', 'BB02_Lwr', 'BB03_Upr', 'BB03_Lwr', 'BB11_Upr', 'BB11_Lwr', 'BB16_Upr', 'BB16_Lwr', 'BB04_Upr', 'BB05_Upr', 'BB1S_Upr', 'BB2S_Upr', 'BBS1_Lwr'],
    BII: ['BA08_Upr', 'BA13_Upr', 'BA16_Upr', 'BA17_Upr', 'BB04_Lwr', 'BB05_Lwr', 'BB07_Upr', 'BB07_Lwr', 'BB08_Upr', 'BB08_Lwr'],
    miB: ['BB10_Upr', 'BB10_Lwr', 'BB12_Upr', 'BB12_Lwr', 'BB13_Upr', 'BB13_Lwr', 'BB14_Upr', 'BB14_Lwr', 'BB15_Upr', 'BB15_Lwr', 'BB20_Upr', 'BB20_Lwr'],
    IC: ['IC01_Upr', 'IC01_Lwr', 'IC02_Upr', 'IC02_Lwr', 'IC03_Upr', 'IC03_Lwr', 'IC04_Upr', 'IC04_Lwr', 'IC05_Upr', 'IC05_Lwr', 'IC06_Upr', 'IC06_Lwr', 'IC07_Upr', 'IC07_Lwr'],
    OPN: ['OP01_Upr', 'OP01_Lwr', 'OP02_Upr', 'OP02_Lwr', 'OP03_Upr', 'OP03_Lwr', 'OP04_Upr', 'OP04_Lwr', 'OP05_Upr', 'OP05_Lwr', 'OP06_Upr', 'OP06_Lwr', 'OP07_Upr', 'OP07_Lwr', 'OP08_Upr', 'OP08_Lwr', 'OP09_Upr', 'OP09_Lwr', 'OP10_Upr', 'OP10_Lwr', 'OP11_Upr', 'OP11_Lwr', 'OP12_Upr', 'OP12_Lwr', 'OP13_Upr', 'OP13_Lwr', 'OP14_Upr', 'OP14_Lwr', 'OP15_Upr', 'OP15_Lwr', 'OP16_Upr', 'OP16_Lwr', 'OP17_Upr', 'OP17_Lwr', 'OP18_Upr', 'OP18_Lwr', 'OP19_Upr', 'OP19_Lwr', 'OP20_Upr', 'OP20_Lwr', 'OP21_Upr', 'OP21_Lwr', 'OP22_Upr', 'OP22_Lwr', 'OP23_Upr', 'OP23_Lwr', 'OP24_Upr', 'OP24_Lwr', 'OP25_Upr', 'OP25_Lwr', 'OP26_Upr', 'OP26_Lwr', 'OP27_Upr', 'OP27_Lwr', 'OP28_Upr', 'OP28_Lwr', 'OP29_Upr', 'OP29_Lwr', 'OP30_Upr', 'OP30_Lwr', 'OP31_Upr', 'OP31_Lwr', 'OPS1_Upr', 'OPS1_Lwr', 'OP1S_Upr', 'OP1S_Lwr'],
    SYN: ['AAS1_Upr', 'AB1S_Lwr', 'AB2S_Lwr', 'BB1S_Lwr', 'BB2S_Lwr', 'BBS1_Upr', 'ZZ1S_Lwr', 'ZZ2S_Lwr', 'ZZS1_Upr', 'ZZS2_Upr'],
    Z: ['ZZ01_Upr', 'ZZ01_Lwr', 'ZZ02_Upr', 'ZZ02_Lwr', 'ZZ1S_Upr', 'ZZ2S_Upr', 'ZZS1_Lwr', 'ZZS2_Lwr'],
    N: ['NANT_Upr', 'NANT_Lwr'],
};
type ConformersByClass = typeof ConformersByClass;
Michal Malý's avatar
Michal Malý committed
type VisualRepresentations = 'ball-and-stick'|'cartoon';
Michal Malý's avatar
Michal Malý committed
const Display = {
Michal Malý's avatar
Michal Malý committed
    representation: 'cartoon' as VisualRepresentations,
Michal Malý's avatar
Michal Malý committed

    showNucleic: true,
    showProtein: false,
    showWater: false,

    showPyramids: true,
Michal Malý's avatar
Michal Malý committed
    pyramidsTransparent: false,

    showBalls: false,
    ballsTransparent: false,
Michal Malý's avatar
Michal Malý committed

    modelNumber: 1,
Michal Malý's avatar
Michal Malý committed

    classColors: { ...NtCColors.Classes },
    conformerColors: { ...NtCColors.Conformers },
Michal Malý's avatar
Michal Malý committed
};
type Display = typeof Display;

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}'),
Michal Malý's avatar
Michal Malý committed
    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 }),
Michal Malý's avatar
Michal Malý committed
    onDeselected: PD.Value(() => {}, { isHidden: true }),
    onSelected: PD.Value((loci: Representation.Loci) => {}, { isHidden: true }),
Michal Malý's avatar
Michal Malý committed
type ReDNATCOLociSelectionProps = PD.Values<typeof ReDNATCOLociSelectionParams>;

const ReDNATCOLociSelectionProvider = PluginBehavior.create({
Michal Malý's avatar
Michal Malý committed
    name: 'rednatco-loci-selection-provider',
    category: 'interaction',
Michal Malý's avatar
Michal Malý committed
    display: { name: 'Interactive step selection' },
    params: () => ReDNATCOLociSelectionParams,
Michal Malý's avatar
Michal Malý committed
    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],
Michal Malý's avatar
Michal Malý committed
                [
                    '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() {
        }
Michal Malý's avatar
Michal Malý committed
        constructor(ctx: PluginContext, params: ReDNATCOLociSelectionProps) {
            super(ctx, params);
            this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
        }
    },
});

class ReDNATCOMspViewer {
Michal Malý's avatar
Michal Malý committed
    private haveMultipleModels = false;
    private steps: StepInfo[] = [];
    private stepNames: Map<string, number> = new Map();

    constructor(public plugin: PluginUIContext, interactionContext: { self?: ReDNATCOMspViewer }) {
        interactionContext.self = this;
Michal Malý's avatar
Michal Malý committed
    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;
    }

Michal Malý's avatar
Michal Malý committed
    private getBuilder(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
        return this.plugin.state.data.build().to(IDs.ID(id, sub, ref));
    }

Michal Malý's avatar
Michal Malý committed
    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;
    }

Michal Malý's avatar
Michal Malý committed
    private ntcRef(step: StepInfo, where: 'sel'|'prev'|'next') {
        return rcref(step.assignedNtC === 'NANT' ? step.closestNtC : step.assignedNtC, where);
Michal Malý's avatar
Michal Malý committed
    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,
                    },
                },
            },
Michal Malý's avatar
Michal Malý committed
    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) {
Michal Malý's avatar
Michal Malý committed
                const loci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(parent.data, parent.data));
                const s = Loci.getBoundingSphere(loci);
Michal Malý's avatar
Michal Malý committed
                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 });
    }

Michal Malý's avatar
Michal Malý committed
    private stepNext(currentIdx: number) {
        if (currentIdx === this.steps.length - 1)
            return void 0;
        const currentStep = this.steps[currentIdx];
        const cand1 = this.steps[currentIdx + 1];
        if (!currentStep.altId2 || !cand1.altId1 || currentStep.altId2 === cand1.altId1)
            return cand1; // Current step is "altId'd" and the candidate step has a matching altId
        if (currentIdx + 2 === this.steps.length)
            return cand1; // Current step is "altId'd", candidate step has a mismatching altId but there are no more steps to try
        const cand2 = this.steps[currentIdx + 2];
        return cand2.resNo1 === currentStep.resNo2 ? cand2 : cand1;
    }

    private stepPrev(currentIdx: number) {
        if (currentIdx === 0)
            return void 0;
        const currentStep = this.steps[currentIdx];
        const cand1 = this.steps[currentIdx - 1];
        if (!currentStep.altId1 || !cand1.altId2 || currentStep.altId1 === cand1.altId2)
            return cand1; // Current step is "altId'd" and the candidate step has a matching altId
        if (currentIdx - 2 < 0)
            return cand1; // Current step is "altId'd", candidate step has a mismatching altId but there are no more steps to try
        const cand2 = this.steps[currentIdx - 2];
        return cand2.resNo2 === currentStep.resNo1 ? cand2 : cand1;
    }

Michal Malý's avatar
Michal Malý committed
    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: 'chain-id', params: { asymId: 'auth' } },
                };
            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: 'chain-id' } },
                };
        }
    }

Michal Malý's avatar
Michal Malý committed
    private superpose(reference: StructureElement.Loci, stru: StructureElement.Loci, altId1?: string, altId2?: string) {
Michal Malý's avatar
Michal Malý committed
        const refElems = dinucleotideBackbone(reference);
Michal Malý's avatar
Michal Malý committed
        const struElems = dinucleotideBackbone(stru, altId1, altId2);
Michal Malý's avatar
Michal Malý committed

        return Superpose.superposition(
            { elements: refElems, conformation: reference.elements[0].unit.conformation },
            { elements: struElems, conformation: stru.elements[0].unit.conformation }
        );
    }

    static async create(target: HTMLElement) {
Michal Malý's avatar
Michal Malý committed
        const interactCtx: { self?: ReDNATCOMspViewer } = { self: undefined };
        const defaultSpec = DefaultPluginUISpec();
        const spec: PluginUISpec = {
            ...defaultSpec,
            behaviors: [
                PluginSpec.Behavior(ReDNATCOLociLabelProvider),
                PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
Michal Malý's avatar
Michal Malý committed
                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;

Michal Malý's avatar
Michal Malý committed
        return new ReDNATCOMspViewer(plugin, interactCtx);
Michal Malý's avatar
Michal Malý committed
    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,
                        },
                    },
                },
Michal Malý's avatar
Michal Malý committed
            })
        );

        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();
            }
Michal Malý's avatar
Michal Malý committed
        } else
Michal Malý's avatar
Michal Malý committed
            await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('pyramids', 'nucleic', BaseRef) });
    }

Michal Malý's avatar
Michal Malý committed
    async changeRepresentation(display: Partial<Display>) {
        const b = this.plugin.state.data.build();
        const repr = display.representation ?? 'cartoon';

        for (const sub of ['nucleic', 'protein', 'water'] as IDs.Substructure[]) {
            if (this.has('visual', sub)) {
                b.to(IDs.ID('visual', sub, BaseRef))
                    .update(
                        StateTransforms.Representation.StructureRepresentation3D,
                        old => ({
                            ...old,
Michal Malý's avatar
Michal Malý committed
                            ...this.substructureVisuals(repr),
Michal Malý's avatar
Michal Malý committed
    gatherStepInfo(): { steps: StepInfo[], stepNames: Map<string, number> }|undefined {
Michal Malý's avatar
Michal Malý committed
        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');
Michal Malý's avatar
Michal Malý committed
        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');
Michal Malý's avatar
Michal Malý committed
        const _stepIds = tableSum.getField('step_id');
        const _assignedNtCs = tableSum.getField('assigned_NtC');
        const _closestNtCs = tableSum.getField('closest_NtC');
Michal Malý's avatar
Michal Malý committed
        if (!_ids || !_names || !_stepIds || !_assignedNtCs || !_closestNtCs || !_labelAltId1 || !_labelAltId2 || !_authSeqId1 || !_authSeqId2) {
Michal Malý's avatar
Michal Malý committed
            console.warn('Expected fields are not present in NtC categories');
            return void 0;
        }

        const ids = _ids.toIntArray();
        const names = _names.toStringArray();
Michal Malý's avatar
Michal Malý committed
        const authSeqId1 = _authSeqId1.toIntArray();
        const authSeqId2 = _authSeqId2.toIntArray();
        const labelAltId1 = _labelAltId1.toStringArray();
        const labelAltId2 = _labelAltId2.toStringArray();
Michal Malý's avatar
Michal Malý committed
        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];
Michal Malý's avatar
Michal Malý committed
                    const resNo1 = authSeqId1[jdx];
                    const resNo2 = authSeqId2[jdx];
                    const altId1 = labelAltId1[jdx] === '' ? void 0 : labelAltId1[jdx];
                    const altId2 = labelAltId2[jdx] === '' ? void 0 : labelAltId2[jdx];
Michal Malý's avatar
Michal Malý committed

                    // We're assuming that steps are ID'd with a contigious, monotonic sequence starting from 1
Michal Malý's avatar
Michal Malý committed
                    steps[id - 1] = { name, assignedNtC, closestNtC, resNo1, resNo2, altId1, altId2 };
Michal Malý's avatar
Michal Malý committed
                    stepNames.set(name, id - 1);
                    break;
                }
            }
        }

        return { steps, stepNames };
    }

Michal Malý's avatar
Michal Malý committed
    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 [];
    }

Michal Malý's avatar
Michal Malý committed
    has(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
        return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj;
    }

Michal Malý's avatar
Michal Malý committed
    isReady() {
        return this.has('structure', '', BaseRef);
    }

Michal Malý's avatar
Michal Malý committed
    async loadStructure(data: string, type: 'pdb'|'cif', display: Partial<Display>) {
Michal Malý's avatar
Michal Malý committed
        // TODO: Remove the currently loaded structure

        const b = (t => type === 'pdb'
Michal Malý's avatar
Michal Malý committed
            ? t.apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
            : t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
Michal Malý's avatar
Michal Malý committed
        )(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', '', BaseRef) }))
Michal Malý's avatar
Michal Malý committed
            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 }, { ref: IDs.ID('model', '', BaseRef) })
Michal Malý's avatar
Michal Malý committed
            .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,
Michal Malý's avatar
Michal Malý committed
                    this.substructureVisuals('cartoon'),
Michal Malý's avatar
Michal Malý committed
                    { ref: IDs.ID('visual', 'nucleic', BaseRef) }
                );
            if (display.showPyramids) {
                bb.to(IDs.ID('structure', 'nucleic', BaseRef))
                    .apply(
                        StateTransforms.Representation.StructureRepresentation3D,
Michal Malý's avatar
Michal Malý committed
                        this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), false),
Michal Malý's avatar
Michal Malý committed
                        { ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
                    );
            }
        }
        if (display.showProtein && this.has('structure', 'protein')) {
            bb.to(IDs.ID('structure', 'protein', BaseRef))
                .apply(
                    StateTransforms.Representation.StructureRepresentation3D,
Michal Malý's avatar
Michal Malý committed
                    this.substructureVisuals('cartoon'),
Michal Malý's avatar
Michal Malý committed
                    { ref: IDs.ID('visual', 'protein', BaseRef) }
                );
        }
        if (display.showWater && this.has('structure', 'water')) {
            bb.to(IDs.ID('structure', 'water', BaseRef))
                .apply(
                    StateTransforms.Representation.StructureRepresentation3D,
Michal Malý's avatar
Michal Malý committed
                    this.substructureVisuals('ball-and-stick'),
Michal Malý's avatar
Michal Malý committed
                    { ref: IDs.ID('visual', 'water', BaseRef) }
                );
        }

        await bb.commit();
Michal Malý's avatar
Michal Malý committed

        this.haveMultipleModels = this.getModelCount() > 1;

Michal Malý's avatar
Michal Malý committed
        const ntcInfo = this.gatherStepInfo();
Michal Malý's avatar
Michal Malý committed
        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();
Michal Malý's avatar
Michal Malý committed
            b.apply(RawData, { data: cfmr, label: `Reference ${c}` }, { ref: IDs.ID('data', '', bRef) })
Michal Malý's avatar
Michal Malý committed
                .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();
    }

    onDeselected() {
        this.plugin.state.data.build()
            .delete(IDs.ID('superposition', '', NtCSupSel))
            .delete(IDs.ID('superposition', '', NtCSupPrev))
            .delete(IDs.ID('superposition', '', NtCSupNext))
            .commit();
    }

    onLociSelected(selected: Representation.Loci) {
        const loci = Loci.normalize(selected.loci, 'two-residues');

Michal Malý's avatar
Michal Malý committed
        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);
            this.superposeReferences(stepName, '', []);
        }
Michal Malý's avatar
Michal Malý committed
    async switchModel(display: Partial<Display>) {
Michal Malý's avatar
Michal Malý committed
        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
                })
            );
Michal Malý's avatar
Michal Malý committed
        await b.commit();
Michal Malý's avatar
Michal Malý committed
    async superposeReferences(stepName: string, 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) } },
            };
        };
Michal Malý's avatar
Michal Malý committed
        const stepDesc = Step.fromName(stepName);
        if (!stepDesc)
            return;
Michal Malý's avatar
Michal Malý committed
        const stepIdx = this.stepNames.get(stepName);
        if (stepIdx === undefined) {
Michal Malý's avatar
Michal Malý committed
            console.error(`Unknown step name ${stepName}`);
            return;
        }

Michal Malý's avatar
Michal Malý committed
        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'
        );
        if (loci.kind !== 'element-loci')
            return;
        const selLoci = Loci.normalize(loci, 'two-residues');
        if (selLoci.kind !== 'element-loci')
            return;

Michal Malý's avatar
Michal Malý committed
        const step = this.steps[stepIdx];
        const stepPrev = this.stepPrev(stepIdx);
        const stepNext = this.stepNext(stepIdx);
Michal Malý's avatar
Michal Malý committed
        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;
Michal Malý's avatar
Michal Malý committed

        if (!ntcRefSel) {
Michal Malý's avatar
Michal Malý committed
            console.error(`stepIdx ${stepIdx} does not map to a known step`);
Michal Malý's avatar
Michal Malý committed
            return;
        }

        const b = this.plugin.state.data.build()
            .delete(IDs.ID('superposition', '', NtCSupSel))
            .delete(IDs.ID('superposition', '', NtCSupPrev))
            .delete(IDs.ID('superposition', '', NtCSupNext));

Michal Malý's avatar
Michal Malý committed
        const addReference = (ntcRef: string, superposRef: string, loci: Loci, altId1: string|undefined, altId2: string|undefined, color: number) => {
Michal Malý's avatar
Michal Malý committed
            const refStru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRef))!.obj!;
            const refLoci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(refStru.data, refStru.data));
Michal Malý's avatar
Michal Malý committed
            if (loci.kind === 'element-loci' && Step.is(loci)) {
Michal Malý's avatar
Michal Malý committed
                const { bTransform, rmsd } = this.superpose(refLoci, loci, altId1, altId2);
Michal Malý's avatar
Michal Malý committed
                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;
Michal Malý's avatar
Michal Malý committed
        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);
        }
Michal Malý's avatar
Michal Malý committed

        b.commit();
Michal Malý's avatar
Michal Malý committed

        return rmsd;
Michal Malý's avatar
Michal Malý committed
    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);
Michal Malý's avatar
Michal Malý committed
            const visuals = this.substructureVisuals(sub === 'water' ? 'ball-and-stick' : repr);
Michal Malý's avatar
Michal Malý committed
            if (b) {
                b.apply(
                    StateTransforms.Representation.StructureRepresentation3D,
Michal Malý's avatar
Michal Malý committed
                    visuals,
Michal Malý's avatar
Michal Malý committed
                    { ref: IDs.ID('visual', sub, BaseRef) }
                );
                await b.commit();
            }
Michal Malý's avatar
Michal Malý committed
        } else {
Michal Malý's avatar
Michal Malý committed
            await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) });
Michal Malý's avatar
Michal Malý committed
            this.resetCameraRadius();
        }
Michal Malý's avatar
Michal Malý committed
interface State {
    display: Display;
Michal Malý's avatar
Michal Malý committed
    showControls: boolean;
Michal Malý's avatar
Michal Malý committed
}
class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
Michal Malý's avatar
Michal Malý committed
    private presentConformers: string[] = [];
    private viewer: ReDNATCOMspViewer|null = null;

Michal Malý's avatar
Michal Malý committed
    private classColorToConformers(k: keyof ConformersByClass, color: Color) {
        const updated: Partial<NtCColors.Conformers> = {};
        ConformersByClass[k].map(cfmr => updated[cfmr as keyof NtCColors.Conformers] = color);

        return updated;