diff --git a/src/apps/rednatco/api-impl.ts b/src/apps/rednatco/api-impl.ts index bed5fbff2daab5e983ce9dae321475cac1b83579..27ce3fd3866ead331a47b5feaeef5e64aed0085d 100644 --- a/src/apps/rednatco/api-impl.ts +++ b/src/apps/rednatco/api-impl.ts @@ -34,7 +34,7 @@ export class ReDNATCOMspApiImpl implements ReDNATCOMspApi.Object { return !!this.target; } - loadStructure(coords: { data: string, type: 'cif' | 'pdb' }, densityMaps: { data: Uint8Array, type: 'ccp4' | 'dsn6', kind: '2fo-fc' | 'fo-fc' | 'em' }[] | null) { + loadStructure(coords: { data: string, type: ReDNATCOMspApi.CoordinatesFormat }, densityMaps: { data: Uint8Array, type: ReDNATCOMspApi.DensityMapFormat, kind: ReDNATCOMspApi.DensityMapKind }[] | null) { this.check(); this.target!.loadStructure(coords, densityMaps); } diff --git a/src/apps/rednatco/api.ts b/src/apps/rednatco/api.ts index 1652681da019ee27c21a111cd1a46773b713ed20..ac0de6d82c1f37a642b77d446689c7f5bd13c44f 100644 --- a/src/apps/rednatco/api.ts +++ b/src/apps/rednatco/api.ts @@ -1,6 +1,10 @@ import { Filters } from './filters'; export namespace ReDNATCOMspApi { + export type CoordinatesFormat = 'cif' | 'pdb'; + export type DensityMapFormat = 'ccp4' | 'dsn6'; + export type DensityMapKind = '2fo-fc' | 'fo-fc' | 'em'; + export namespace Payloads { export type StepSelection = { name: string; @@ -126,7 +130,7 @@ export namespace ReDNATCOMspApi { event: (evt: Event) => void; init: (elemId: string, onEvent?: (evt: Event) => void) => void; isReady: () => boolean; - loadStructure: (coords: { data: string, type: 'cif' | 'pdb'}, densityMaps: { data: Uint8Array, type: 'ccp4' | 'dsn6', kind: '2fo-fc' | 'fo-fc' | 'em' }[] | null) => void; + loadStructure: (coords: { data: string, type: CoordinatesFormat }, densityMaps: { data: Uint8Array, type: DensityMapFormat, kind: DensityMapKind }[] | null) => void; query: <T extends Queries.Type>(type: T) => ResponseTypes[T]; } } diff --git a/src/apps/rednatco/density-map-controls.tsx b/src/apps/rednatco/density-map-controls.tsx index 2fee14728e04283d07c7452fd75a26024066f43f..17f7659ce6bed326ee3d3851bf25230dcf9761ee 100644 --- a/src/apps/rednatco/density-map-controls.tsx +++ b/src/apps/rednatco/density-map-controls.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { ReDNATCOMspApi as Api } from './api'; import { ColorPicker } from './color-picker'; import { ColorBox, RangeSlider, SpinBox, ToggleButton } from './controls'; -import { DensityMapDisplay, DensityMapKind } from './index'; +import { DensityMapDisplay } from './index'; import { isoBounds, isoToFixed } from './util'; import { ReDNATCOMspViewer as Viewer } from './viewer'; import { Color } from '../../mol-util/color'; @@ -135,7 +136,7 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props return ctrls; } - private mapName(kind: DensityMapKind) { + private mapName(kind: Api.DensityMapKind) { switch (kind) { case '2fo-fc': return <span>2Fo-Fc</span>; diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx index 7ff9c2329c3c10f05a548124d8efd465b7c2ae2b..8553321ca68c6173cb57d930df3fa39d1b2efdd5 100644 --- a/src/apps/rednatco/index.tsx +++ b/src/apps/rednatco/index.tsx @@ -31,21 +31,20 @@ const ConformersByClass = { }; type ConformersByClass = typeof ConformersByClass; -type ToolBarItems = 'structure'|'ntc'|'colors'|'density-maps'; +type ToolBarItems = 'structure' | 'ntc' | 'colors' | 'density-maps'; const ViewerToolBar = ToolBar.Specialize<ToolBarItems>(); const DefaultChainColor = Color(0xD9D9D9); const DefaultDensityMapAlpha = 0.25; const DefaultWaterColor = Color(0x0BB2FF); -export type VisualRepresentations = 'ball-and-stick'|'cartoon'; -export type DensityMapRepresentation = 'wireframe'|'solid'; -export type DensityMapKind = '2fo-fc'|'fo-fc'|'em'; +export type VisualRepresentations = 'ball-and-stick' | 'cartoon' | 'ntc-tube'; +export type DensityMapRepresentation = 'wireframe' | 'solid'; export const DefaultDensityDifferencePositiveColor = Color(0x00C300); export const DefaultDensityDifferenceNegativeColor = Color(0xC30000); export const DefaultDensityMapColor = Color(0x009DFF); const DefaultDensityMapDisplay = { - kind: '2fo-fc' as DensityMapKind, + kind: '2fo-fc' as Api.DensityMapKind, representations: [] as DensityMapRepresentation[], isoValue: 0, @@ -56,18 +55,17 @@ export type DensityMapDisplay = typeof DefaultDensityMapDisplay; const Display = { structures: { - representation: 'cartoon' as VisualRepresentations, - + nucleicRepresentation: 'cartoon' as VisualRepresentations, showNucleic: true, + + proteinRepresentation: 'cartoon' as Omit<VisualRepresentations, 'ntc-tube'>, showProtein: false, + showWater: false, showPyramids: true, pyramidsTransparent: false, - showBalls: false, - ballsTransparent: false, - modelNumber: 1, classColors: { ...NtCColors.Classes }, @@ -79,12 +77,6 @@ const Display = { }; export type Display = typeof Display; -function capitalize(s: string) { - if (s.length === 0) - return s; - return s[0].toLocaleUpperCase() + s.slice(1); -} - interface State { display: Display; showControls: boolean; @@ -92,8 +84,8 @@ interface State { export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { private currentFilter: Filters.All = Filters.Empty(); private presentConformers: string[] = []; - private viewer: ReDNATCOMspViewer|undefined = undefined; - private selectedStep: Api.Payloads.StepSelection|undefined = undefined; + private viewer: ReDNATCOMspViewer | undefined = undefined; + private selectedStep: Api.Payloads.StepSelection | undefined = undefined; constructor(props: ReDNATCOMsp.Props) { super(props); @@ -111,6 +103,17 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { return updated; } + private finalizeNtCColorUpdate(display: Display) { + this.viewer!.changeNtCColors(display).then(() => { + if (display.structures.showNucleic && display.structures.nucleicRepresentation === 'ntc-tube') { + this.viewer!.changeChainColor(['nucleic'], display).then(() => { + this.setState({ ...this.state, display }); + }); + } else + this.setState({ ...this.state, display }); + }); + } + private updateChainColor(color: number) { const display: Display = { ...this.state.display, @@ -120,11 +123,11 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { }, }; - this.viewer!.changeChainColor(display); + this.viewer!.changeChainColor(['nucleic', 'protein'], display); this.setState({ ...this.state, display }); } - private updateClassColor(changes: { cls: keyof NtCColors.Classes, color: number }|{ cls: keyof NtCColors.Classes, color: number }[]) { + private updateClassColor(changes: { cls: keyof NtCColors.Classes, color: number } | { cls: keyof NtCColors.Classes, color: number }[]) { const classColors = { ...this.state.display.structures.classColors }; const isArray = Array.isArray(changes); @@ -144,11 +147,10 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { display.structures.classColors = classColors; display.structures.conformerColors = conformerColors; - this.viewer!.changeNtCColors(display); - this.setState({ ...this.state, display }); + this.finalizeNtCColorUpdate(display); } - private updateConformerColor(changes: { conformer: keyof NtCColors.Conformers, color: number }|{ conformer: keyof NtCColors.Conformers, color: number }[]) { + private updateConformerColor(changes: { conformer: keyof NtCColors.Conformers, color: number } | { conformer: keyof NtCColors.Conformers, color: number }[]) { const conformerColors = { ...this.state.display.structures.conformerColors }; if (Array.isArray(changes)) changes.forEach(item => conformerColors[item.conformer] = Color(item.color)); @@ -158,8 +160,7 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { const display = { ...this.state.display }; display.structures.conformerColors = conformerColors; - this.viewer!.changeNtCColors(display); - this.setState({ ...this.state, display }); + this.finalizeNtCColorUpdate(display); } private updateWaterColor(color: number) { @@ -191,6 +192,10 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { if (!this.viewer) return; + for (const p in cmd) { + console.log(`${p}: ${cmd[p as keyof typeof cmd]}`); + } + if (cmd.type === 'redraw') window.dispatchEvent(new Event('resize')); else if (cmd.type === 'deselect-step') { @@ -233,7 +238,7 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { } } - loadStructure(coords: { data: string, type: 'pdb'|'cif' }, densityMaps: { data: Uint8Array, type: 'ccp4'|'dsn6', kind: '2fo-fc'|'fo-fc'|'em' }[]|null) { + loadStructure(coords: { data: string, type: Api.CoordinatesFormat }, densityMaps: { data: Uint8Array, type: Api.DensityMapFormat, kind: Api.DensityMapKind }[] | null) { if (this.viewer) { const display = { ...this.state.display }; if (densityMaps) { @@ -306,38 +311,80 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { icon: './imgs/nucleic.svg', content: <ToolBarContent style={{ width: '10em' }}> + <div className='rmsp-control-line'> + <div className='rmsp-control-item'> + <ToggleButton + text='Nucleic' + enabled={hasNucleic} + switchedOn={this.state.display.structures.showNucleic} + onClicked={() => { + const display = { ...this.state.display }; + display.structures.showNucleic = !display.structures.showNucleic, + this.viewer!.toggleSubstructure('nucleic', display).then(() => { + this.setState({ ...this.state, display }); + }); + }} + /> + </div> + </div> <div className='rmsp-control-line'> <div className='rmsp-control-item'> <PushButton - text={capitalize(this.state.display.structures.representation)} - enabled={ready} + text='Cartoon' + enabled={ready && this.state.display.structures.showNucleic} onClicked={() => { const display = { ...this.state.display }; - display.structures.representation = display.structures.representation === 'cartoon' ? 'ball-and-stick' : 'cartoon'; - this.viewer!.changeRepresentation(display); - this.setState({ ...this.state, display }); + if (display.structures.nucleicRepresentation !== 'cartoon') { + display.structures.nucleicRepresentation = 'cartoon'; + this.viewer!.changeRepresentation('nucleic', display).then(() => { + this.setState({ ...this.state, display }); + }); + } }} /> </div> </div> - - <div className='rmsp-control-vertical-spacer' /> - <div className='rmsp-control-line'> <div className='rmsp-control-item'> - <ToggleButton - text='Nucleic' - enabled={hasNucleic} - switchedOn={this.state.display.structures.showNucleic} + <PushButton + text='Ball-and-stick' + enabled={ready && this.state.display.structures.showNucleic} onClicked={() => { const display = { ...this.state.display }; - display.structures.showNucleic = !display.structures.showNucleic, - this.viewer!.toggleSubstructure('nucleic', display); - this.setState({ ...this.state, display }); + if (display.structures.nucleicRepresentation !== 'ball-and-stick') { + display.structures.nucleicRepresentation = 'ball-and-stick'; + this.viewer!.changeRepresentation('nucleic', display).then(() => { + this.setState({ ...this.state, display }); + }); + } + }} + /> + </div> + </div> + <div className='rmsp-control-line'> + <div className='rmsp-control-item'> + <PushButton + text='NtC tube' + enabled={ready && this.state.display.structures.showNucleic} + onClicked={() => { + const display = { ...this.state.display }; + if (display.structures.nucleicRepresentation !== 'ntc-tube') { + display.structures.nucleicRepresentation = 'ntc-tube'; + this.viewer!.changeRepresentation('nucleic', display).then(() => { + display.structures.showPyramids = false; + + this.viewer!.changePyramids(display).then(() => { + this.setState({ ...this.state, display }); + }); + }); + } }} /> </div> </div> + + <div className='rmsp-control-vertical-spacer' /> + <div className='rmsp-control-line'> <div className='rmsp-control-item'> <ToggleButton @@ -347,12 +394,50 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { onClicked={() => { const display = { ...this.state.display }; display.structures.showProtein = !display.structures.showProtein, - this.viewer!.toggleSubstructure('protein', display); - this.setState({ ...this.state, display }); + this.viewer!.toggleSubstructure('protein', display).then(() => { + this.setState({ ...this.state, display }); + }); }} /> </div> </div> + <div className='rmsp-control-line'> + <div className='rmsp-control-item'> + <PushButton + text='Cartoon' + enabled={ready && this.state.display.structures.showProtein} + onClicked={() => { + const display = { ...this.state.display }; + if (display.structures.proteinRepresentation !== 'cartoon') { + display.structures.proteinRepresentation = 'cartoon'; + this.viewer!.changeRepresentation('protein', display).then(() => { + this.setState({ ...this.state, display }); + }); + } + }} + /> + </div> + </div> + <div className='rmsp-control-line'> + <div className='rmsp-control-item'> + <PushButton + text='Ball-and-stick' + enabled={ready && this.state.display.structures.showProtein} + onClicked={() => { + const display = { ...this.state.display }; + if (display.structures.proteinRepresentation !== 'ball-and-stick') { + display.structures.proteinRepresentation = 'ball-and-stick'; + this.viewer!.changeRepresentation('protein', display).then(() => { + this.setState({ ...this.state, display }); + }); + } + }} + /> + </div> + </div> + + <div className='rmsp-control-vertical-spacer' /> + <div className='rmsp-control-line'> <div className='rmsp-control-item'> <ToggleButton @@ -362,8 +447,9 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { onClicked={() => { const display = { ...this.state.display }; display.structures.showWater = !this.state.display.structures.showWater; - this.viewer!.toggleSubstructure('water', display); - this.setState({ ...this.state, display }); + this.viewer!.toggleSubstructure('water', display).then(() => { + this.setState({ ...this.state, display }); + }); }} /> </div> @@ -387,8 +473,9 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { onClicked={() => { const display = { ...this.state.display }; display.structures.showPyramids = !display.structures.showPyramids; - this.viewer!.changePyramids(display); - this.setState({ ...this.state, display }); + this.viewer!.changePyramids(display).then(() => { + this.setState({ ...this.state, display }); + }); }} /> </div> @@ -401,39 +488,9 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { onClicked={() => { const display = { ...this.state.display }; display.structures.pyramidsTransparent = !display.structures.pyramidsTransparent; - this.viewer!.changePyramids(display); - this.setState({ ...this.state, display }); - }} - /> - </div> - </div> - - <div className='rmsp-control-vertical-spacer' /> - - <div className='rmsp-control-vertical-section-caption'> - Balls - </div> - <div className='rmsp-control-line'> - <div className='rmsp-control-item'> - <ToggleButton - text='Balls' - enabled={false} - switchedOn={false} - onClicked={() => {}} - /> - </div> - </div> - <div className='rmsp-control-line'> - <div className='rmsp-control-item'> - <PushButton - text={this.state.display.structures.ballsTransparent ? 'Transp.' : 'Solid'} - enabled={this.state.display.structures.showBalls} - onClicked={() => { - const display = { ...this.state.display }; - display.structures.showBalls = !display.structures.showBalls; - - /* No balls today... */ - this.setState({ ...this.state, display }); + this.viewer!.changePyramids(display).then(() => { + this.setState({ ...this.state, display }); + }); }} /> </div> diff --git a/src/apps/rednatco/traverse.ts b/src/apps/rednatco/traverse.ts index 547a81d758deed809eba5d53e4d5b2f27c99e7c5..ec907d85a62ff88df8216b3e3392ee898c9bc6fa 100644 --- a/src/apps/rednatco/traverse.ts +++ b/src/apps/rednatco/traverse.ts @@ -23,7 +23,7 @@ export namespace Traverse { // TODO: We will be able to use a function from DnatcoUtils once it gets upstreamed const _loc = StructureElement.Location.create(); - export function findResidue(asymId: string, seqId: number, altId: string|undefined, insCode: string, loci: StructureElement.Loci, source: 'label'|'auth') { + export function findResidue(asymId: string, seqId: number, altId: string | undefined, insCode: string, loci: StructureElement.Loci, source: 'label' | 'auth') { _loc.structure = loci.structure; for (const e of loci.elements) { _loc.unit = e.unit; @@ -76,9 +76,9 @@ export namespace Traverse { export function findStep( asymId: string, - seqId1: number, altId1: string|undefined, insCode1: string, - seqId2: number, altId2: string|undefined, insCode2: string, - loci: StructureElement.Loci, source: 'label'|'auth' + seqId1: number, altId1: string | undefined, insCode1: string, + seqId2: number, altId2: string | undefined, insCode2: string, + loci: StructureElement.Loci, source: 'label' | 'auth' ) { const first = findResidue(asymId, seqId1, altId1, insCode1, loci, source); if (first.kind === 'empty-loci') diff --git a/src/apps/rednatco/viewer.ts b/src/apps/rednatco/viewer.ts index 5bc5ced8cd728d63c2020905f3da23b493be08e2..2e3ea7b4be0175a777f44d729ddd3562e4721489 100644 --- a/src/apps/rednatco/viewer.ts +++ b/src/apps/rednatco/viewer.ts @@ -11,12 +11,14 @@ import { Superpose } from './superpose'; import { Traverse } from './traverse'; import { isoBounds, prettyIso } from './util'; import { DnatcoNtCs } from '../../extensions/dnatco'; +import { DnatcoTypes } from '../../extensions/dnatco/types'; +import { NtCTubeTypes } from '../../extensions/dnatco/ntc-tube/types'; 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, StructureElement, StructureProperties, StructureSelection, Trajectory } from '../../mol-model/structure'; +import { ElementIndex, Model, Structure, StructureElement, StructureProperties, StructureSelection, Trajectory } from '../../mol-model/structure'; import { Volume } from '../../mol-model/volume'; import { structureUnion, structureSubtract } from '../../mol-model/structure/query/utils/structure-set'; import { Location } from '../../mol-model/structure/structure/element/location'; @@ -35,6 +37,9 @@ import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec'; import { Representation } from '../../mol-repr/representation'; import { StateObjectCell, StateObject, StateTransformer } from '../../mol-state'; import { StateBuilder } from '../../mol-state/state/builder'; +import { Script } from '../../mol-script/script'; +import { MolScriptBuilder as MSB } from '../../mol-script/language/builder'; +import { formatMolScript } from '../../mol-script/language/expression-formatter'; import { lociLabel } from '../../mol-theme/label'; import { arrayMax } from '../../mol-util/array'; import { Binding } from '../../mol-util/binding'; @@ -57,6 +62,31 @@ const NtCSupSel = 'ntc-sup-sel'; const NtCSupNext = 'ntc-sup-next'; const SphereBoundaryHelper = new BoundaryHelper('98'); +function ntcStepToElementLoci(step: DnatcoTypes.Step, stru: Structure) { + let expr = MSB.core.rel.eq([MSB.struct.atomProperty.macromolecular.auth_asym_id(), step.auth_asym_id_1]); + expr = MSB.core.logic.and([ + MSB.core.rel.eq([MSB.struct.atomProperty.macromolecular.auth_seq_id(), step.auth_seq_id_1]), + expr + ]); + expr = MSB.core.logic.and([ + MSB.core.rel.eq([MSB.struct.atomProperty.macromolecular.label_alt_id(), step.label_alt_id_1]), + expr + ]); + expr = MSB.struct.generator.atomGroups({ 'atom-test': expr, 'group-by': MSB.struct.atomProperty.macromolecular.label_asym_id() }); + + return Loci.normalize( + Script.toLoci( + Script(formatMolScript(expr), 'mol-script'), + stru + ), + 'two-residues' + ); +} + +function rcref(c: string, where: 'sel' | 'prev' | 'next' | '' = '') { + return `${RCRef}-${c}-${where}`; +} + function superpositionAtomsIndices(loci: StructureElement.Loci) { const es = loci.elements[0]; const loc = Location.create(loci.structure, es.unit, es.unit.elements[OrderedSet.getAt(es.indices, 0)]); @@ -120,8 +150,16 @@ function superpositionAtomsIndices(loci: StructureElement.Loci) { return indices; } -function rcref(c: string, where: 'sel' | 'prev' | 'next' | '' = '') { - return `${RCRef}-${c}-${where}`; +function visualForSubstructure(sub: IDs.Substructure, display: Display) { + if (sub === 'nucleic') { + return display.structures.nucleicRepresentation === 'ntc-tube' + ? SubstructureVisual.NtC('ntc-tube', display.structures.conformerColors) + : SubstructureVisual.BuiltIn(display.structures.nucleicRepresentation, Color(display.structures.chainColor)); + } else if (sub === 'protein') { + return SubstructureVisual.BuiltIn(display.structures.proteinRepresentation, Color(display.structures.chainColor)); + } else /* water */ { + return SubstructureVisual.BuiltIn('ball-and-stick', Color(display.structures.waterColor)); + } } const ReDNATCOLociLabelProvider = PluginBehavior.create({ @@ -199,6 +237,9 @@ const ReDNATCOLociSelectionProvider = PluginBehavior.create({ this.ctx.managers.interactivity.lociSelects.deselectAll(); if (current.loci.kind === 'element-loci') { this.params.onSelected(current); + } else if (current.loci.kind === 'data-loci') { + console.log(current.loci.tag, current.loci.data); + this.params.onSelected(current); } }, lociIsNotEmpty @@ -233,6 +274,28 @@ const ReDNATCOLociSelectionProvider = PluginBehavior.create({ }, }); +export namespace SubstructureVisual { + export type BuiltIn = { + type: 'built-in', + repr: Omit<VisualRepresentations, 'ntc-tube'>, + color: Color + } + export function BuiltIn(repr: BuiltIn['repr'], color: BuiltIn['color']): BuiltIn { + return { type: 'built-in', repr, color }; + } + + export type NtC = { + type: 'ntc', + repr: 'ntc-tube', + colors: NtCColors.Conformers + } + export function NtC(repr: NtC['repr'], colors: NtC['colors']): NtC { + return { type: 'ntc', repr, colors }; + } + + export type Types = BuiltIn | NtC; +} + export class ReDNATCOMspViewer { private haveMultipleModels = false; private steps: Step.ExtendedDescription[] = []; @@ -374,29 +437,50 @@ export class ReDNATCOMspViewer { return this.steps[idx]; } - private substructureVisuals(representation: 'ball-and-stick' | 'cartoon', color: Color) { - switch (representation) { - case 'cartoon': - return { - type: { - name: 'cartoon', - params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false }, - }, - colorTheme: { name: 'uniform', params: { value: color } } - }; - case 'ball-and-stick': - return { - type: { - name: 'ball-and-stick', - params: { - sizeFactor: 0.2, - sizeAspectRatio: 0.35, - excludeTypes: ['hydrogen-bond', 'aromatic'], - aromaticBonds: false, + private substructureVisuals(visual: SubstructureVisual.Types) { + if (visual.type === 'built-in') { + switch (visual.repr) { + case 'cartoon': + return { + type: { + name: 'cartoon', + params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false }, }, - }, - colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'custom', params: color } } }, - }; + colorTheme: { name: 'uniform', params: { value: visual.color } } + }; + case 'ball-and-stick': + return { + type: { + name: 'ball-and-stick', + params: { + sizeFactor: 0.2, + sizeAspectRatio: 0.35, + excludeTypes: ['hydrogen-bond', 'aromatic'], + aromaticBonds: false, + }, + }, + colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'custom', params: visual.color } } }, + }; + } + } else if (visual.type === 'ntc') { + switch (visual.repr) { + case 'ntc-tube': + return { + type: { + name: 'ntc-tube', + params: {}, + }, + colorTheme: { + name: 'ntc-tube', + params: { + colors: { + name: 'custom', + params: visual.colors, + }, + }, + }, + }; + } } } @@ -410,13 +494,13 @@ export class ReDNATCOMspViewer { ); } - private async toggleNucleicSubstructure(show: boolean, repr: VisualRepresentations, color: Color) { + private async toggleNucleicSubstructure(show: boolean, visual: SubstructureVisual.Types) { if (this.has('structure', 'remainder-slice', BaseRef)) { const b = this.getBuilder('structure', 'remainder-slice'); if (show) { b.apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals(repr, color), + this.substructureVisuals(visual), { ref: IDs.ID('visual', 'remainder-slice', BaseRef) } ); } else @@ -429,7 +513,7 @@ export class ReDNATCOMspViewer { if (show) { b.apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals(repr, color), + this.substructureVisuals(visual), { ref: IDs.ID('visual', 'nucleic', BaseRef) } ); } else @@ -515,18 +599,21 @@ export class ReDNATCOMspViewer { return new ReDNATCOMspViewer(plugin, interactCtx, app); } - async changeChainColor(display: Display) { + async changeChainColor(subs: IDs.Substructure[], display: Display) { + const b = this.plugin.state.data.build(); + const color = Color(display.structures.chainColor); - const b = this.plugin.state.data.build(); - for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) { + for (const sub of subs) { + const vis = visualForSubstructure(sub, display); + if (this.has('visual', sub)) { b.to(IDs.ID('visual', sub, BaseRef)) .update( StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.substructureVisuals(display.structures.representation, color), + ...this.substructureVisuals(vis), }) ); } @@ -538,17 +625,20 @@ export class ReDNATCOMspViewer { StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.substructureVisuals('ball-and-stick', color), + ...this.substructureVisuals(SubstructureVisual.BuiltIn('ball-and-stick', color)), }) ); } + if (this.has('visual', 'remainder-slice', BaseRef)) { + const vis = visualForSubstructure('nucleic', display); + b.to(IDs.ID('visual', 'remainder-slice', BaseRef)) .update( StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.substructureVisuals(display.structures.representation, color), + ...this.substructureVisuals(vis), }) ); } @@ -625,35 +715,34 @@ export class ReDNATCOMspViewer { } } - async changeRepresentation(display: Display) { + async changeRepresentation(sub: IDs.Substructure, display: Display) { const b = this.plugin.state.data.build(); - const repr = display.structures.representation; - const color = Color(display.structures.chainColor); + const vis = visualForSubstructure(sub, display); - for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) { - if (this.has('visual', sub)) { - b.to(IDs.ID('visual', sub, BaseRef)) + if (this.has('visual', sub)) { + b.to(IDs.ID('visual', sub, BaseRef)) + .update( + StateTransforms.Representation.StructureRepresentation3D, + old => ({ + ...old, + ...this.substructureVisuals(vis), + }) + ); + } + + if (sub === 'nucleic') { + if (this.has('visual', 'remainder-slice', BaseRef)) { + b.to(IDs.ID('visual', 'remainder-slice', BaseRef)) .update( StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.substructureVisuals(repr, color), + ...this.substructureVisuals(vis), }) ); } } - if (this.has('visual', 'remainder-slice', BaseRef)) { - b.to(IDs.ID('visual', 'remainder-slice', BaseRef)) - .update( - StateTransforms.Representation.StructureRepresentation3D, - old => ({ - ...old, - ...this.substructureVisuals(repr, color), - }) - ); - } - await b.commit(); } @@ -878,8 +967,8 @@ export class ReDNATCOMspViewer { } async loadStructure( - coords: { data: string, type: 'pdb' | 'cif' }, - densityMaps: { data: Uint8Array, type: 'ccp4' | 'dsn6', kind: '2fo-fc' | 'fo-fc' | 'em' }[] | null, + coords: { data: string, type: Api.CoordinatesFormat }, + densityMaps: { data: Uint8Array, type: Api.DensityMapFormat, kind: Api.DensityMapKind }[] | null, display: Display ) { // TODO: Remove the currently loaded structure @@ -936,7 +1025,7 @@ export class ReDNATCOMspViewer { b3.to(IDs.ID('structure', 'nucleic', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals('cartoon', chainColor), + this.substructureVisuals(SubstructureVisual.BuiltIn('cartoon', chainColor)), { ref: IDs.ID('visual', 'nucleic', BaseRef) } ); if (display.structures.showPyramids) { @@ -952,7 +1041,7 @@ export class ReDNATCOMspViewer { b3.to(IDs.ID('structure', 'protein', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals('cartoon', chainColor), + this.substructureVisuals(SubstructureVisual.BuiltIn('cartoon', chainColor)), { ref: IDs.ID('visual', 'protein', BaseRef) } ); } @@ -1060,10 +1149,30 @@ export class ReDNATCOMspViewer { } async onLociSelected(selected: Representation.Loci) { - const loci = Loci.normalize(selected.loci, 'two-residues'); - - if (loci.kind === 'element-loci') { - const stepDesc = Step.describe(loci, this.haveMultipleModels); + const normalized = (() => { + if (selected.loci.kind === 'data-loci') { + if (selected.loci.tag === 'dnatco-tube-segment-data') { + const stru = this.plugin.state.data.cells.get(IDs.ID('entire-structure', 'nucleic', BaseRef)); + if (stru) { + const tubeLoci = selected.loci as NtCTubeTypes.Loci; + const stepIdx = tubeLoci.elements[0] / 4; // There are 4 tube segments per step + const step = tubeLoci.data[stepIdx]; + if (step) + return ntcStepToElementLoci(step, stru.obj!.data); + else + return EmptyLoci; + } + return EmptyLoci; + } else + return EmptyLoci; + } else if (selected.loci.kind === 'element-loci') + return Loci.normalize(selected.loci, 'two-residues'); + else + return EmptyLoci; + })(); + + if (normalized.kind === 'element-loci') { + const stepDesc = Step.describe(normalized, this.haveMultipleModels); if (stepDesc && this.stepNames.has(stepDesc.name)) this.notifyStepSelected(stepDesc.name); } @@ -1118,8 +1227,6 @@ export class ReDNATCOMspViewer { .commit(); await this.toggleSubstructure('nucleic', display); - - this.resetCameraRadius(); } async actionSelectStep(stepSel: Api.Payloads.StepSelection, prevSel: Api.Payloads.StepSelection | undefined, nextSel: Api.Payloads.StepSelection | undefined, display: Display) { @@ -1172,7 +1279,7 @@ export class ReDNATCOMspViewer { ) .apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals('ball-and-stick', chainColor), + this.substructureVisuals(SubstructureVisual.BuiltIn('ball-and-stick', chainColor)), { ref: IDs.ID('visual', 'selected-slice', BaseRef) } ) .to(entireStruCell) @@ -1184,10 +1291,14 @@ export class ReDNATCOMspViewer { // Only show the remainder if the nucleic substructure is shown if (display.structures.showNucleic) { + const vis = display.structures.nucleicRepresentation === 'ntc-tube' + ? SubstructureVisual.NtC('ntc-tube', display.structures.conformerColors) + : SubstructureVisual.BuiltIn(display.structures.nucleicRepresentation, Color(display.structures.chainColor)); + b.to(IDs.ID('structure', 'remainder-slice', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals(display.structures.representation, chainColor), + this.substructureVisuals(vis), { ref: IDs.ID('visual', 'remainder-slice', BaseRef) } ) .delete(IDs.ID('visual', 'nucleic', BaseRef)); @@ -1284,27 +1395,43 @@ export class ReDNATCOMspViewer { } async toggleSubstructure(sub: IDs.Substructure, display: Display) { - const show = sub === 'nucleic' ? !!display.structures.showNucleic : - sub === 'protein' ? !!display.structures.showProtein : !!display.structures.showWater; - const repr = display.structures.representation; - - if (sub === 'nucleic') - this.toggleNucleicSubstructure(show, repr, Color(display.structures.chainColor)); - else { - if (show) { + if (sub === 'nucleic') { + const show = display.structures.showNucleic; + const vis = display.structures.nucleicRepresentation === 'ntc-tube' + ? SubstructureVisual.NtC('ntc-tube', display.structures.conformerColors) + : SubstructureVisual.BuiltIn(display.structures.nucleicRepresentation, Color(display.structures.chainColor)); + + await this.toggleNucleicSubstructure(show, vis); + this.resetCameraRadius(); + } else if (sub === 'protein') { + if (!display.structures.showProtein) { + await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) }); + this.resetCameraRadius(); + } else { const b = this.getBuilder('structure', sub); - const visuals = sub === 'water' ? this.waterVisuals(Color(display.structures.waterColor)) : this.substructureVisuals(repr, Color(display.structures.chainColor)); if (b) { b.apply( StateTransforms.Representation.StructureRepresentation3D, - visuals, + this.substructureVisuals(SubstructureVisual.BuiltIn(display.structures.proteinRepresentation, display.structures.chainColor)), { ref: IDs.ID('visual', sub, BaseRef) } ); await b.commit(); } - } else { + } + } else if (sub === 'water') { + if (!display.structures.showWater) { await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) }); this.resetCameraRadius(); + } else { + const b = this.getBuilder('structure', sub); + if (b) { + b.apply( + StateTransforms.Representation.StructureRepresentation3D, + this.waterVisuals(display.structures.waterColor), + { ref: IDs.ID('visual', sub, BaseRef) } + ); + await b.commit(); + } } } } diff --git a/src/extensions/dnatco/behavior.ts b/src/extensions/dnatco/behavior.ts index bbe33f8e9c5c8d700a08926962303d2bc5a7e620..e47d71450aada4f584808877e9a1b25c8d3f43db 100644 --- a/src/extensions/dnatco/behavior.ts +++ b/src/extensions/dnatco/behavior.ts @@ -56,4 +56,3 @@ export const DnatcoNtCs = PluginBehavior.create<{ autoAttach: boolean, showToolT showToolTip: PD.Boolean(true) }) }); -