diff --git a/src/apps/rednatco/controls.tsx b/src/apps/rednatco/controls.tsx index 11ee3a5b36b3c31cc7a9caf2db2d73d154a639e5..2c3c53896964c687c7ec6e10969c31fb2351dbda 100644 --- a/src/apps/rednatco/controls.tsx +++ b/src/apps/rednatco/controls.tsx @@ -56,6 +56,34 @@ export class ToggleButton extends React.Component<{ text: string, enabled: boole } } +export class RangeSlider extends React.Component<RangeSlider.Props> { + render() { + return ( + <input + className='rmsp-range-slider' + type='range' + value={this.props.value ? this.props.value : 0} + min={this.props.min} + max={this.props.max} + step={this.props.step} + onChange={evt => { + const n = parseFloat(evt.currentTarget.value); + this.props.onChange(isNaN(n) ? null : n); + }} + /> + ); + } +} +export namespace RangeSlider { + export interface Props { + min: number; + max: number; + step: number; + value: number|null; + onChange: (n: number|null) => void; + } +} + export class SpinBox extends React.Component<SpinBox.Props> { private clsDisabled() { return this.props.classNameDisabled ?? 'rmsp-spinbox-input-disabled'; @@ -88,7 +116,14 @@ export class SpinBox extends React.Component<SpinBox.Props> { type='text' className={this.props.disabled ? this.clsDisabled() : this.clsEnabled()} value={this.props.formatter ? this.props.formatter(this.props.value) : this.props.value?.toString() ?? ''} - onChange={evt => this.props.onChange(evt.currentTarget.value)} + onChange={evt =>{ + const v = evt.currentTarget.value; + const n = parseFloat(v); + if (!isNaN(n) && (n < this.props.min || n > this.props.max)) + return; + + this.props.onChange(evt.currentTarget.value); + }} onWheel={evt => { evt.stopPropagation(); if (this.props.value === null) diff --git a/src/apps/rednatco/density-map-controls.tsx b/src/apps/rednatco/density-map-controls.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b922006a04cc4f67f5bbbd9dae4cfe27e4d7315e --- /dev/null +++ b/src/apps/rednatco/density-map-controls.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { CollapsibleVertical, RangeSlider, SpinBox, ToggleButton } from './controls'; +import { isoToFixed } from './util'; + +export class DensityMapControls extends React.Component<DensityMapControls.Props> { + render() { + return ( + <CollapsibleVertical caption='Density map'> + <div className='rmsp-controls'> + <div className='rmsp-controls-section-caption'> + Representations: + </div> + <div className='rmsp-controls-line'> + <div className='rmsp-control-item'> + <ToggleButton + text='Wireframe' + switchedOn={this.props.wireframe} + onClick={() => this.props.toggleWireframe()} + enabled={true} + /> + </div> + <div className='rmsp-control-item'> + <ToggleButton + text='Solid' + switchedOn={this.props.solid} + onClick={() => this.props.toggleSolid()} + enabled={true} + /> + </div> + </div> + + <div className='rmsp-controls-section-caption'> + Iso: + </div> + <div className='rmsp-controls-line'> + <div className='rmsp-control-item'> + <RangeSlider + min={this.props.isoMin} + max={this.props.isoMax} + step={this.props.isoStep} + value={isoToFixed(this.props.iso, this.props.isoStep)} + onChange={(v) => this.props.changeIso(v!)} + /> + </div> + <div className='rmsp-control-item'> + <div style={{ display: 'grid', gridTemplateColumns: '4em 1fr' }}> + <SpinBox + min={this.props.isoMin} + max={this.props.isoMax} + step={this.props.isoStep} + value={isoToFixed(this.props.iso, this.props.isoStep)} + onChange={(v) => this.props.changeIso(parseFloat(v))} + pathPrefix='' + /> + <div /> + </div> + </div> + </div> + + <div className='rmsp-controls-section-caption'> + Transparency: + </div> + <div className='rmsp-controls-line'> + <div className='rmsp-control-item'> + <RangeSlider + min={0} + max={1} + step={0.1} + value={(1.0 - this.props.alpha)} + onChange={(v) => this.props.changeAlpha(1.0 - v!)} + /> + </div> + <div className='rmsp-control-item'> + <div style={{ display: 'grid', gridTemplateColumns: '4em 1fr' }}> + <SpinBox + min={0} + max={1} + step={0.1} + value={parseFloat((1.0 - this.props.alpha).toFixed(1))} + onChange={(v) => this.props.changeAlpha(1.0 - parseFloat(v))} + pathPrefix='' + /> + </div> + </div> + </div> + </div> + </CollapsibleVertical> + ); + } +} + +export namespace DensityMapControls { + export interface Props { + wireframe: boolean; + solid: boolean; + toggleWireframe: () => void; + toggleSolid: () => void; + + isoMin: number; + isoMax: number; + isoStep: number; + iso: number; + changeIso: (iso: number) => void; + + alpha: number; + changeAlpha: (alpha: number) => void; + } +} diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx index e5a86b0aec8797fff1391b05bc594f2ba5b9d0a7..de537d688e735662f2cd77ceed309d7515c5202e 100644 --- a/src/apps/rednatco/index.tsx +++ b/src/apps/rednatco/index.tsx @@ -2,12 +2,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { ReDNATCOMspApi as Api } from './api'; import { ReDNATCOMspApiImpl } from './api-impl'; +import { DensityMapControls } from './density-map-controls'; import { Filters } from './filters'; import { ReDNATCOMspViewer } from './viewer'; import { NtCColors } from './colors'; import { ColorPicker } from './color-picker'; import { CollapsibleVertical, PushButton, ToggleButton } from './controls'; -import { luminance } from './util'; +import { isoBounds, luminance, toggleArray } from './util'; import { Color } from '../../mol-util/color'; import { assertUnreachable } from '../../mol-util/type-helpers'; import './index.html'; @@ -26,27 +27,39 @@ const ConformersByClass = { type ConformersByClass = typeof ConformersByClass; const DefaultChainColor = Color(0xD9D9D9); +const DefaultDensityMapAlpha = 0.5; const DefaultWaterColor = Color(0x0BB2FF); export type VisualRepresentations = 'ball-and-stick'|'cartoon'; +export type DensityMapRepresentation = 'wireframe'|'solid'; + const Display = { - representation: 'cartoon' as VisualRepresentations, + structures: { + representation: 'cartoon' as VisualRepresentations, + + showNucleic: true, + showProtein: false, + showWater: false, - showNucleic: true, - showProtein: false, - showWater: false, + showPyramids: true, + pyramidsTransparent: false, - showPyramids: true, - pyramidsTransparent: false, + showBalls: false, + ballsTransparent: false, - showBalls: false, - ballsTransparent: false, + modelNumber: 1, - modelNumber: 1, + classColors: { ...NtCColors.Classes }, + conformerColors: { ...NtCColors.Conformers }, + chainColor: DefaultChainColor, + waterColor: DefaultWaterColor, + }, + densityMap: { + representations: ['wireframe'] as DensityMapRepresentation[], + isoValue: 0, - classColors: { ...NtCColors.Classes }, - conformerColors: { ...NtCColors.Conformers }, - chainColor: DefaultChainColor, - waterColor: DefaultWaterColor, + alpha: DefaultDensityMapAlpha, + color: 0x000000 + }, }; export type Display = typeof Display; @@ -99,7 +112,10 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { private updateChainColor(color: number) { const display: Display = { ...this.state.display, - chainColor: Color(color), + structures: { + ...this.state.display.structures, + chainColor: Color(color), + }, }; this.viewer!.changeChainColor(display); @@ -107,7 +123,7 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { } private updateClassColor(changes: { cls: keyof NtCColors.Classes, color: number }|{ cls: keyof NtCColors.Classes, color: number }[]) { - const classColors = { ...this.state.display.classColors }; + const classColors = { ...this.state.display.structures.classColors }; const isArray = Array.isArray(changes); if (isArray) { @@ -116,7 +132,7 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { classColors[changes.cls] = Color(changes.color); const conformerColors: NtCColors.Conformers = { - ...this.state.display.conformerColors, + ...this.state.display.structures.conformerColors, ...(isArray ? changes.map(item => this.classColorToConformers(item.cls, Color(item.color))) : this.classColorToConformers(changes.cls, Color(changes.color))) @@ -128,7 +144,7 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { } private updateConformerColor(changes: { conformer: keyof NtCColors.Conformers, color: number }|{ conformer: keyof NtCColors.Conformers, color: number }[]) { - const conformerColors = { ...this.state.display.conformerColors }; + const conformerColors = { ...this.state.display.structures.conformerColors }; if (Array.isArray(changes)) changes.forEach(item => conformerColors[item.conformer] = Color(item.color)); else @@ -142,7 +158,10 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { private updateWaterColor(color: number) { const display: Display = { ...this.state.display, - waterColor: Color(color), + structures: { + ...this.state.display.structures, + waterColor: Color(color), + }, }; this.viewer!.changeWaterColor(display); @@ -196,10 +215,13 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { const display: Display = { ...this.state.display, - modelNumber: cmd.model, + structures: { + ...this.state.display.structures, + modelNumber: cmd.model, + }, }; - this.viewer.switchModel(display); + this.viewer.switchModel(display.structures.modelNumber); this.setState({ ...this.state, display }); } } @@ -244,6 +266,9 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { const hasProtein = this.viewer?.has('structure', 'protein') ?? false; const hasWater = this.viewer?.has('structure', 'water') ?? false; + const isoRange = this.viewer?.densityMapIsoRange(); + const _isoBounds = isoRange ? isoBounds(isoRange.min, isoRange.max) : { min: 0, max: 0, step: 0 }; + return ( <div className='rmsp-app'> <div id={this.props.elemId + '-viewer'} className='rmsp-viewer'></div> @@ -253,12 +278,16 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { <div className='rmsp-controls-line'> <div className='rmsp-control-item'> <PushButton - text={capitalize(this.state.display.representation)} + text={capitalize(this.state.display.structures.representation)} enabled={ready} onClick={() => { const display: Display = { ...this.state.display, - representation: this.state.display.representation === 'cartoon' ? 'ball-and-stick' : 'cartoon', + structures: { + ...this.state.display.structures, + representation: this.state.display.structures.representation === 'cartoon' + ? 'ball-and-stick' : 'cartoon', + }, }; this.viewer!.changeRepresentation(display); this.setState({ ...this.state, display }); @@ -273,11 +302,14 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { <ToggleButton text='Nucleic' enabled={hasNucleic} - switchedOn={this.state.display.showNucleic} + switchedOn={this.state.display.structures.showNucleic} onClick={() => { const display: Display = { ...this.state.display, - showNucleic: !this.state.display.showNucleic, + structures: { + ...this.state.display.structures, + showNucleic: !this.state.display.structures.showNucleic, + } }; this.viewer!.toggleSubstructure('nucleic', display); this.setState({ ...this.state, display }); @@ -288,11 +320,14 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { <ToggleButton text='Protein' enabled={hasProtein} - switchedOn={this.state.display.showProtein} + switchedOn={this.state.display.structures.showProtein} onClick={() => { const display: Display = { ...this.state.display, - showProtein: !this.state.display.showProtein, + structures: { + ...this.state.display.structures, + showProtein: !this.state.display.structures.showProtein, + }, }; this.viewer!.toggleSubstructure('protein', display); this.setState({ ...this.state, display }); @@ -303,11 +338,14 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { <ToggleButton text='Water' enabled={hasWater} - switchedOn={this.state.display.showWater} + switchedOn={this.state.display.structures.showWater} onClick={() => { const display: Display = { ...this.state.display, - showWater: !this.state.display.showWater, + structures: { + ...this.state.display.structures, + showWater: !this.state.display.structures.showWater, + }, }; this.viewer!.toggleSubstructure('water', display); this.setState({ ...this.state, display }); @@ -323,11 +361,14 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { <ToggleButton text='Pyramids' enabled={ready} - switchedOn={this.state.display.showPyramids} + switchedOn={this.state.display.structures.showPyramids} onClick={() => { const display: Display = { ...this.state.display, - showPyramids: !this.state.display.showPyramids, + structures: { + ...this.state.display.structures, + showPyramids: !this.state.display.structures.showPyramids, + } }; this.viewer!.changePyramids(display); this.setState({ ...this.state, display }); @@ -336,12 +377,15 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { </div> <div className='rmsp-control-item'> <PushButton - text={this.state.display.pyramidsTransparent ? 'Transparent' : 'Solid'} - enabled={this.state.display.showPyramids} + text={this.state.display.structures.pyramidsTransparent ? 'Transparent' : 'Solid'} + enabled={this.state.display.structures.showPyramids} onClick={() => { const display: Display = { ...this.state.display, - pyramidsTransparent: !this.state.display.pyramidsTransparent, + structures: { + ...this.state.display.structures, + pyramidsTransparent: !this.state.display.structures.pyramidsTransparent, + } }; this.viewer!.changePyramids(display); this.setState({ ...this.state, display }); @@ -360,12 +404,15 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { </div> <div className='rmsp-control-item'> <PushButton - text={this.state.display.ballsTransparent ? 'Transparent' : 'Solid'} - enabled={this.state.display.showBalls} + text={this.state.display.structures.ballsTransparent ? 'Transparent' : 'Solid'} + enabled={this.state.display.structures.showBalls} onClick={() => { const display: Display = { ...this.state.display, - ballsTransparent: !this.state.display.ballsTransparent, + structures: { + ...this.state.display.structures, + ballsTransparent: !this.state.display.structures.ballsTransparent, + }, }; /* No balls today... */ this.setState({ ...this.state, display }); @@ -383,11 +430,11 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { className='rmsp-control-item' onClick={evt => ColorPicker.create( evt, - this.state.display.classColors[k], + this.state.display.structures.classColors[k], color => this.updateClassColor({ cls: k, color }) )} > - <ColorBox caption={k} color={this.state.display.classColors[k]} /> + <ColorBox caption={k} color={this.state.display.structures.classColors[k]} /> </div> <PushButton text='R' @@ -411,21 +458,21 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { className='rmsp-control-item' onClick={evt => ColorPicker.create( evt, - this.state.display.conformerColors[uprKey], + this.state.display.structures.conformerColors[uprKey], color => this.updateConformerColor({ conformer: uprKey, color }) )} > - <ColorBox caption={`${ntc.slice(0, 2)}`} color={this.state.display.conformerColors[uprKey]} /> + <ColorBox caption={`${ntc.slice(0, 2)}`} color={this.state.display.structures.conformerColors[uprKey]} /> </div> <div className='rmsp-control-item' onClick={evt => ColorPicker.create( evt, - this.state.display.conformerColors[lwrKey], + this.state.display.structures.conformerColors[lwrKey], color => this.updateConformerColor({ conformer: lwrKey, color }) )} > - <ColorBox caption={`${ntc.slice(2)}`} color={this.state.display.conformerColors[lwrKey]} /> + <ColorBox caption={`${ntc.slice(2)}`} color={this.state.display.structures.conformerColors[lwrKey]} /> </div> <PushButton text='R' @@ -450,11 +497,11 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { className='rmsp-control-item' onClick={evt => ColorPicker.create( evt, - this.state.display.chainColor, + this.state.display.structures.chainColor, color => this.updateChainColor(color) )} > - <ColorBox caption='Chains' color={this.state.display.chainColor} /> + <ColorBox caption='Chains' color={this.state.display.structures.chainColor} /> </div> <PushButton text='R' @@ -467,11 +514,11 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { className='rmsp-control-item' onClick={evt => ColorPicker.create( evt, - this.state.display.waterColor, + this.state.display.structures.waterColor, color => this.updateWaterColor(color) )} > - <ColorBox caption='Waters' color={this.state.display.waterColor} /> + <ColorBox caption='Waters' color={this.state.display.structures.waterColor} /> </div> <PushButton text='R' @@ -483,6 +530,49 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { </div> </div> </CollapsibleVertical> + {this.viewer?.hasDensityMap() + ? <DensityMapControls + wireframe={this.state.display.densityMap.representations.includes('wireframe')} + toggleWireframe={() => { + const display = { ...this.state.display }; + display.densityMap.representations = toggleArray(display.densityMap.representations, 'wireframe'); + + this.viewer!.changeDensityMap(display); + this.setState({ ...this.state, display }); + }} + + solid={this.state.display.densityMap.representations.includes('solid')} + toggleSolid={() => { + const display = { ...this.state.display }; + display.densityMap.representations = toggleArray(display.densityMap.representations, 'solid'); + + this.viewer!.changeDensityMap(display); + this.setState({ ...this.state, display }); + }} + + isoMin={_isoBounds.min} + isoMax={_isoBounds.max} + isoStep={_isoBounds.step} + iso={this.state.display.densityMap.isoValue} + changeIso={(v) => { + const display = { ...this.state.display }; + display.densityMap.isoValue = v; + + this.viewer!.changeDensityMap(display); + this.setState({ ...this.state, display }); + }} + + alpha={this.state.display.densityMap.alpha} + changeAlpha={(alpha) => { + const display = { ...this.state.display }; + display.densityMap.alpha = alpha; + + this.viewer!.changeDensityMap(display); + this.setState({ ...this.state, display }); + }} + /> + : undefined + } </div> ); } diff --git a/src/apps/rednatco/rednatco-molstar.css b/src/apps/rednatco/rednatco-molstar.css index c048b92c09079c3f5555a9629c0f83e1000a2832..fddb76fdec5905731c5c7e0360845e52e3139919 100644 --- a/src/apps/rednatco/rednatco-molstar.css +++ b/src/apps/rednatco/rednatco-molstar.css @@ -122,6 +122,44 @@ margin: 0.15em; } +.rmsp-range-slider { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + + background: #ccc; + height: 0.15em; + margin: auto 0 auto 0; + width: 100%; +} +.rmsp-range-slider::-moz-range-thumb { + background: white; + border-radius: 0; + border: 0.15em solid #aaa; + cursor: pointer; + height: 0.8em; + transition: background-color 222ms; + width: 0.8em; +} +.rmsp-range-slider::-moz-range-thumb:hover { + background-color: #ccc; +} +.rmsp-range-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + + background: white; + border-radius: 0; + border: 0.15em solid #aaa; + cursor: pointer; + height: 1em; + transition: background-color 222ms; + width: 1em; +} +.rmsp-range-slider::-webkit-slider-thumb:hover { + background-color: #ccc; +} + .rmsp-spinbox-container { background-color: white; border: 0.15em solid #ccc; diff --git a/src/apps/rednatco/util.ts b/src/apps/rednatco/util.ts index 75a79ad263d76aba80f03fd7e9b69e86fb828cbe..165709e39b5f8bb96f1018ca2146b8c5f0ef4215 100644 --- a/src/apps/rednatco/util.ts +++ b/src/apps/rednatco/util.ts @@ -1,5 +1,27 @@ import { Color } from '../../mol-util/color'; +export function isoBounds(min: number, max: number): { min: number, max: number, step: number } { + let diff = max - min; + if (diff <= 0.0) + return { min, max, step: 0.01 }; // This should never happen + + diff /= 25; + const l = Math.floor(Math.log10(diff)); + const step = Math.pow(10.0, l); + + min = Math.floor((min - step) / step) * step; + max = Math.floor((max + step) / step) * step; + + return { min, max, step }; +} + +export function isoToFixed(iso: number, step: number) { + const d = Math.log10(step); + if (d >= 0) + return parseFloat(iso.toFixed(0)); + return parseFloat(iso.toFixed(-d)); +} + /* https://alienryderflex.com/hsp.html */ export function luminance(color: Color) { let [r, g, b] = Color.toRgb(color); @@ -8,3 +30,16 @@ export function luminance(color: Color) { b /= 255.0; return Math.sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); } + +export function prettyIso(iso: number, step: number) { + return Math.floor((iso - step) / step) * step + step; +} + +export function toggleArray<T>(array: T[], elem: T) { + if (array.includes(elem)) + return array.filter((x) => x !== elem); + else { + array.push(elem); + return array; + } +} diff --git a/src/apps/rednatco/viewer.ts b/src/apps/rednatco/viewer.ts index 8c609374fee3ae872e6fbffe55983d5f0b12afa3..209e78ddc96ed9e767850fe0ddac9e93d5d7e45e 100644 --- a/src/apps/rednatco/viewer.ts +++ b/src/apps/rednatco/viewer.ts @@ -9,6 +9,7 @@ import { ReferenceConformersPdbs } from './reference-conformers-pdbs'; import { Step } from './step'; import { Superpose } from './superpose'; import { Traverse } from './traverse'; +import { isoBounds, prettyIso } from './util'; import { DnatcoConfalPyramids } from '../../extensions/dnatco'; import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation'; import { OrderedSet } from '../../mol-data/int/ordered-set'; @@ -16,7 +17,7 @@ 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 { Volume } from '../../mol-model/volume'; +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'; import { MmcifFormat } from '../../mol-model-formats/structure/mmcif'; @@ -243,6 +244,24 @@ export class ReDNATCOMspViewer { this.app = app; } + private densityMapVisuals(vis: Display['densityMap']) { + return { + type: { + name: 'isosurface', + params: { + alpha: vis.alpha, + isoValue: Volume.IsoValue.absolute(vis.isoValue), + visuals: vis.representations, + sizeFactor: 0.75, + } + }, + colorTheme: { + name: 'uniform', + params: { value: Color(vis.color) }, + }, + }; + } + private focusOnLoci(loci: StructureElement.Loci) { if (!this.plugin.canvas3d) return; @@ -489,7 +508,7 @@ export class ReDNATCOMspViewer { } async changeChainColor(display: Display) { - const color = Color(display.chainColor); + const color = Color(display.structures.chainColor); const b = this.plugin.state.data.build(); for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) { @@ -499,7 +518,7 @@ export class ReDNATCOMspViewer { StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.substructureVisuals(display.representation, color), + ...this.substructureVisuals(display.structures.representation, color), }) ); } @@ -521,7 +540,7 @@ export class ReDNATCOMspViewer { StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.substructureVisuals(display.representation, color), + ...this.substructureVisuals(display.structures.representation, color), }) ); } @@ -543,7 +562,7 @@ export class ReDNATCOMspViewer { params: { colors: { name: 'custom', - params: display.conformerColors ?? NtCColors.Conformers, + params: display.structures.conformerColors ?? NtCColors.Conformers, }, }, }, @@ -554,13 +573,13 @@ export class ReDNATCOMspViewer { } async changePyramids(display: Display) { - if (display.showPyramids) { + if (display.structures.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), + this.pyramidsParams(display.structures.conformerColors ?? NtCColors.Conformers, new Map(), display.structures.pyramidsTransparent ?? false), { ref: IDs.ID('pyramids', 'nucleic', BaseRef) } ); await b.commit(); @@ -571,7 +590,7 @@ export class ReDNATCOMspViewer { StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - ...this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false), + ...this.pyramidsParams(display.structures.conformerColors ?? NtCColors.Conformers, new Map(), display.structures.pyramidsTransparent ?? false), }) ); await b.commit(); @@ -581,7 +600,7 @@ export class ReDNATCOMspViewer { } async changeWaterColor(display: Display) { - const color = Color(display.waterColor); + const color = Color(display.structures.waterColor); const b = this.plugin.state.data.build(); if (this.has('visual', 'water', BaseRef)) { @@ -600,8 +619,8 @@ export class ReDNATCOMspViewer { async changeRepresentation(display: Display) { const b = this.plugin.state.data.build(); - const repr = display.representation; - const color = Color(display.chainColor); + const repr = display.structures.representation; + const color = Color(display.structures.chainColor); for (const sub of ['nucleic', 'protein'] as IDs.Substructure[]) { if (this.has('visual', sub)) { @@ -630,6 +649,24 @@ export class ReDNATCOMspViewer { await b.commit(); } + async changeDensityMap(display: Display) { + if (!this.hasDensityMap()) + return; + + const b = this.plugin.state.data.build().to(IDs.DensityID('visual', BaseRef)); + const vis = display.densityMap; + + b.update( + StateTransforms.Representation.VolumeRepresentation3D, + old => ({ + ...old, + ...this.densityMapVisuals(vis), + }) + ); + + await b.commit(); + } + currentModelNumber() { const model = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj; if (!model) @@ -637,6 +674,15 @@ export class ReDNATCOMspViewer { return (model as StateObject<Model>).data.modelNum; } + densityMapIsoRange(ref = BaseRef): { min: number, max: number }|undefined { + const cell = this.plugin.state.data.cells.get(IDs.DensityID('volume', ref)); + if (!cell || !cell.obj) + return void 0; + + const grid = (cell.obj.data as Volume).grid; + return { min: grid.stats.min, max: grid.stats.max }; + } + focusOnSelectedStep() { const sel = this.plugin.state.data.cells.get(IDs.ID('superposition', '', NtCSupSel)); const prev = this.plugin.state.data.cells.get(IDs.ID('superposition', '', NtCSupPrev)); @@ -796,6 +842,10 @@ export class ReDNATCOMspViewer { return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj?.data; } + hasDensityMap(ref = BaseRef) { + return !!this.plugin.state.data.cells.get(IDs.DensityID('volume', ref))?.obj?.data; + } + isReady() { return this.has('structure', '', BaseRef); } @@ -807,14 +857,14 @@ export class ReDNATCOMspViewer { ) { // TODO: Remove the currently loaded structure - const chainColor = Color(display.chainColor); - const waterColor = Color(display.waterColor); + const chainColor = Color(display.structures.chainColor); + const waterColor = Color(display.structures.waterColor); const b = (t => coords.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: coords.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.ModelFromTrajectory, { modelIndex: display.structures.modelNumber ? display.structures.modelNumber - 1 : 0 }, { ref: IDs.ID('model', '', BaseRef) }) .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('entire-structure', '', BaseRef) }) // Extract substructures .apply(StateTransforms.Model.StructureComplexElement, { type: 'nucleic' }, { ref: IDs.ID('entire-structure', 'nucleic', BaseRef) }) @@ -855,23 +905,23 @@ export class ReDNATCOMspViewer { // Create default visuals const b3 = this.plugin.state.data.build(); - if (display.showNucleic && this.has('structure', 'nucleic')) { + if (display.structures.showNucleic && this.has('structure', 'nucleic')) { b3.to(IDs.ID('structure', 'nucleic', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, this.substructureVisuals('cartoon', chainColor), { ref: IDs.ID('visual', 'nucleic', BaseRef) } ); - if (display.showPyramids) { + if (display.structures.showPyramids) { b3.to(IDs.ID('structure', 'nucleic', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), false), + this.pyramidsParams(display.structures.conformerColors ?? NtCColors.Conformers, new Map(), false), { ref: IDs.ID('pyramids', 'nucleic', BaseRef) } ); } } - if (display.showProtein && this.has('structure', 'protein')) { + if (display.structures.showProtein && this.has('structure', 'protein')) { b3.to(IDs.ID('structure', 'protein', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, @@ -879,7 +929,7 @@ export class ReDNATCOMspViewer { { ref: IDs.ID('visual', 'protein', BaseRef) } ); } - if (display.showWater && this.has('structure', 'water')) { + if (display.structures.showWater && this.has('structure', 'water')) { b3.to(IDs.ID('structure', 'water', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, @@ -899,27 +949,24 @@ export class ReDNATCOMspViewer { b4.toRoot() .apply(RawData, { data: densityMap.data }, { ref: IDs.DensityID('data', BaseRef) }) .apply(ParseTransform) - .apply(VolumeTransform, {}, { ref: IDs.DensityID('volume', BaseRef) }) + .apply(VolumeTransform, {}, { ref: IDs.DensityID('volume', BaseRef) }); + + await b4.commit(); // Load the density map now so we can probe the stats; + const isoRange = this.densityMapIsoRange()!; + const bounds = isoBounds(isoRange.min, isoRange.max); + const iso = prettyIso(((isoRange.max - isoRange.min) / 2) + isoRange.min, bounds.step); + + const b5 = this.plugin.state.data.build().to(IDs.DensityID('volume', BaseRef)) .apply( StateTransforms.Representation.VolumeRepresentation3D, - { - type: { - name: 'isosurface', - params: { - alpha: 0.5, - visuals: ['wireframe'], - sizeFactor: 0.75, - } - }, - colorTheme: { - name: 'uniform', - params: { value: Color(0x000000) }, - }, - }, + this.densityMapVisuals(display.densityMap), { ref: IDs.DensityID('visual', BaseRef) } ); - await b4.commit(); + await b5.commit(); + + display.densityMap.representations = ['wireframe']; + display.densityMap.isoValue = iso; } this.haveMultipleModels = this.getModelCount() > 1; @@ -1034,7 +1081,7 @@ export class ReDNATCOMspViewer { // Switch to a different model if the selected step is from a different model // This is the first thing we need to do if (step.model !== this.currentModelNumber()) - await this.switchModel({ modelNumber: step.model }); + await this.switchModel(step.model); const entireStruCell = this.plugin.state.data.cells.get(IDs.ID('structure', 'nucleic', BaseRef)); if (!entireStruCell) @@ -1066,7 +1113,7 @@ export class ReDNATCOMspViewer { const subtracted = structureSubtract(stru, slice); const remainderBundle = StructureElement.Bundle.fromSubStructure(stru, subtracted); - const chainColor = Color(display.chainColor); + const chainColor = Color(display.structures.chainColor); const b = this.plugin.state.data.build(); b.to(entireStruCell) .apply( @@ -1087,11 +1134,11 @@ export class ReDNATCOMspViewer { ); // Only show the remainder if the nucleic substructure is shown - if (display.showNucleic) { + if (display.structures.showNucleic) { b.to(IDs.ID('structure', 'remainder-slice', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - this.substructureVisuals(display.representation, chainColor), + this.substructureVisuals(display.structures.representation, chainColor), { ref: IDs.ID('visual', 'remainder-slice', BaseRef) } ) .delete(IDs.ID('visual', 'nucleic', BaseRef)); @@ -1108,8 +1155,8 @@ export class ReDNATCOMspViewer { return true; } - async switchModel(display: Partial<Display>) { - if (display.modelNumber && display.modelNumber === this.currentModelNumber()) + async switchModel(modelNumber?: number) { + if (modelNumber && modelNumber === this.currentModelNumber()) return; const b = this.plugin.state.data.build() @@ -1121,7 +1168,7 @@ export class ReDNATCOMspViewer { StateTransforms.Model.ModelFromTrajectory, old => ({ ...old, - modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 + modelIndex: modelNumber ? modelNumber - 1 : 0 }) ); @@ -1181,16 +1228,16 @@ export class ReDNATCOMspViewer { } async toggleSubstructure(sub: IDs.Substructure, display: Display) { - const show = sub === 'nucleic' ? !!display.showNucleic : - sub === 'protein' ? !!display.showProtein : !!display.showWater; - const repr = display.representation; + 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.chainColor)); + this.toggleNucleicSubstructure(show, repr, Color(display.structures.chainColor)); else { if (show) { const b = this.getBuilder('structure', sub); - const visuals = sub === 'water' ? this.waterVisuals(Color(display.waterColor)) : this.substructureVisuals(repr, Color(display.chainColor)); + const visuals = sub === 'water' ? this.waterVisuals(Color(display.structures.waterColor)) : this.substructureVisuals(repr, Color(display.structures.chainColor)); if (b) { b.apply( StateTransforms.Representation.StructureRepresentation3D,