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(); } } } }