diff --git a/src/apps/rednatco/api-impl.ts b/src/apps/rednatco/api-impl.ts index fa36829f0dd9f3091b37389f0013c2d141db6b54..d9940fa8a9aa682c23732779d988afc26e2e226a 100644 --- a/src/apps/rednatco/api-impl.ts +++ b/src/apps/rednatco/api-impl.ts @@ -34,9 +34,9 @@ export class ReDNATCOMspApiImpl implements ReDNATCOMspApi.Object { return !!this.target; } - loadStructure(coords: { data: string, type: 'cif'|'pdb' }, densityMap: { data: Uint8Array, type: 'ccp4'|'dsn6'|'ds' }|null) { + loadStructure(coords: { data: string, type: 'cif'|'pdb' }, densityMaps: { data: Uint8Array, type: 'ccp4'|'dsn6', kind: '2fo-fc'|'fo-fc'|'em' }[]|null) { this.check(); - this.target!.loadStructure(coords, densityMap); + this.target!.loadStructure(coords, densityMaps); } query<T extends ReDNATCOMspApi.Queries.Type>(type: T): ReDNATCOMspApi.ResponseTypes[T] { diff --git a/src/apps/rednatco/api.ts b/src/apps/rednatco/api.ts index 07d281d5a2d2a82b5a18bf1e5b5915b9fa862fd8..09a30b75b6e6a3223376caf251c4952643874e06 100644 --- a/src/apps/rednatco/api.ts +++ b/src/apps/rednatco/api.ts @@ -126,7 +126,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'}, densityMap: { data: Uint8Array, type: 'ccp4'|'dsn6'|'ds' }|null) => void; + loadStructure: (coords: { data: string, type: 'cif'|'pdb'}, densityMaps: { data: Uint8Array, type: 'ccp4'|'dsn6', kind: '2fo-fc'|'fo-fc'|'em' }[]|null) => void; query: <T extends Queries.Type>(type: T) => ResponseTypes[T]; } } diff --git a/src/apps/rednatco/controls.tsx b/src/apps/rednatco/controls.tsx index 5c720989e87a9eb47959b79fdc2775e699001344..572d184203435c52797a7ab78c7fda5eda641234 100644 --- a/src/apps/rednatco/controls.tsx +++ b/src/apps/rednatco/controls.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { fuzzyCmp, numDecimals, reduceDecimals, stof } from './util'; +import { fuzzyCmp, luminance, numDecimals, reduceDecimals, stof } from './util'; +import { Color } from '../../mol-util/color'; const Zero = '0'.charCodeAt(0); const Nine = '9'.charCodeAt(0); @@ -57,6 +58,20 @@ export namespace CollapsibleVertical { } } +export class ColorBox extends React.Component<{ caption: string, color: Color }> { + render() { + const lum = luminance(this.props.color); + return ( + <div + className='rmsp-color-box' + style={{ backgroundColor: Color.toStyle(this.props.color) }} + > + <span style={{ color: lum > 0.6 ? 'black' : 'white', margin: '0.15em' }}>{this.props.caption}</span> + </div> + ); + } +} + export class PushButton extends React.Component<{ text: string, enabled: boolean, onClick: () => void }> { render() { return ( diff --git a/src/apps/rednatco/density-map-controls.tsx b/src/apps/rednatco/density-map-controls.tsx index 750a3066e1ec638b1d54ddd9e1262325f734a8d7..e454a042511b16b17a4e2eb77d108f915e6a63e1 100644 --- a/src/apps/rednatco/density-map-controls.tsx +++ b/src/apps/rednatco/density-map-controls.tsx @@ -1,32 +1,74 @@ import React from 'react'; -import { CollapsibleVertical, RangeSlider, SpinBox, ToggleButton } from './controls'; -import { isoToFixed } from './util'; +import { ColorPicker } from './color-picker'; +import { CollapsibleVertical, ColorBox, RangeSlider, SpinBox, ToggleButton } from './controls'; +import { DensityMapDisplay, DensityMapKind } from './index'; +import { isoBounds, isoToFixed } from './util'; +import { ReDNATCOMspViewer as Viewer } from './viewer'; +import { Color } from '../../mol-util/color'; export class DensityMapControls extends React.Component<DensityMapControls.Props> { - render() { - return ( - <CollapsibleVertical caption='Density map'> - <div className='rmsp-controls'> + private colors(index: number, colors: DensityMapDisplay['colors']) { + const elems = new Array<JSX.Element>(); + + for (let idx = 0; idx < colors.length; idx++) { + const c = colors[idx]; + const e = + <div className='rmsp-control-item' + key={idx} + style={{ backgroundColor: Color.toHexString(c.color) }} + onClick={(evt) => { + ColorPicker.create( + evt, + c.color, + (color) => { + colors[idx] = { ...colors[idx], color: Color(color) }, + this.props.changeColors(index, colors); + } + ); + }} + > + <ColorBox + caption={c.name} + color={c.color} + /> + </div>; + elems.push(e); + } + + return elems; + } + + private controls(display: DensityMapDisplay[]) { + const ctrls = new Array<JSX.Element>(); + + for (let idx = 0; idx < display.length; idx++) { + const isoRange = this.props.viewer.densityMapIsoRange(idx); + const _isoBounds = isoRange ? isoBounds(isoRange.min, isoRange.max) : { min: 0, max: 0, step: 0 }; + + const d = display[idx]; + const elem = ( + <React.Fragment key={idx}> <div className='rmsp-controls-section-caption'> - Representations: + {this.mapName(d.kind)} </div> <div className='rmsp-controls-line'> <div className='rmsp-control-item'> <ToggleButton text='Wireframe' - switchedOn={this.props.wireframe} - onClick={() => this.props.toggleWireframe()} + switchedOn={d.representations.includes('wireframe')} + onClick={() => this.props.toggleWireframe(idx)} enabled={true} /> </div> <div className='rmsp-control-item'> <ToggleButton text='Solid' - switchedOn={this.props.solid} - onClick={() => this.props.toggleSolid()} + switchedOn={d.representations.includes('solid')} + onClick={() => this.props.toggleSolid(idx)} enabled={true} /> </div> + { this.colors(idx, d.colors) } </div> <div className='rmsp-controls-section-caption'> @@ -35,22 +77,22 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props <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!)} + min={_isoBounds.min} + max={_isoBounds.max} + step={_isoBounds.step} + value={isoToFixed(d.isoValue, _isoBounds.step)} + onChange={(v) => this.props.changeIso(idx, v!)} /> </div> - <div className='rmsp-control-item'> + <div className='rmsp-control-item-squished'> <div style={{ display: 'grid', gridTemplateColumns: '4em 1fr' }}> <SpinBox - min={this.props.isoMin} - max={this.props.isoMax} - step={this.props.isoStep} - maxNumDecimals={Math.log10(this.props.isoStep) >= 0 ? 0 : -Math.log10(this.props.isoStep)} - value={isoToFixed(this.props.iso, this.props.isoStep)} - onChange={(n) => this.props.changeIso(n)} + min={_isoBounds.min} + max={_isoBounds.max} + step={_isoBounds.step} + maxNumDecimals={Math.log10(_isoBounds.step) >= 0 ? 0 : -Math.log10(_isoBounds.step)} + value={isoToFixed(d.isoValue, _isoBounds.step)} + onChange={(n) => this.props.changeIso(idx, n)} pathPrefix='' /> <div /> @@ -67,24 +109,48 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props min={0} max={100} step={1} - value={(1.0 - this.props.alpha) * 100} - onChange={(n) => this.props.changeAlpha(1.0 - (n! / 100))} + value={(1.0 - d.alpha) * 100} + onChange={(n) => this.props.changeAlpha(idx, 1.0 - (n! / 100))} /> </div> - <div className='rmsp-control-item'> + <div className='rmsp-control-item-squished'> <div style={{ display: 'grid', gridTemplateColumns: '4em 1fr' }}> <SpinBox min={0} max={100} step={1} maxNumDecimals={0} - value={(1.0 - this.props.alpha) * 100} - onChange={(n) => this.props.changeAlpha(1.0 - (n / 100))} + value={(1.0 - d.alpha) * 100} + onChange={(n) => this.props.changeAlpha(idx, 1.0 - (n / 100))} pathPrefix='' /> </div> </div> </div> + </React.Fragment> + ); + ctrls.push(elem); + } + + return ctrls; + } + + private mapName(kind: DensityMapKind) { + switch (kind) { + case '2fo-fc': + return <span>2Fo-Fc</span>; + case 'fo-fc': + return <span>Fo-Fc</span>; + case 'em': + return <span>EM map</span>; + } + } + + render() { + return ( + <CollapsibleVertical caption='Density map'> + <div className='rmsp-controls'> + {this.controls(this.props.display)} </div> </CollapsibleVertical> ); @@ -93,18 +159,13 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props 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; + viewer: Viewer; + display: DensityMapDisplay[]; - alpha: number; - changeAlpha: (alpha: number) => void; + toggleWireframe: (index: number) => void; + toggleSolid: (index: number) => void; + changeIso: (index: number, iso: number) => void; + changeAlpha: (index: number, alpha: number) => void; + changeColors: (index: number, colors: DensityMapDisplay['colors']) => void; } } diff --git a/src/apps/rednatco/idents.ts b/src/apps/rednatco/idents.ts index 91b5b4c91d599df50d24dd472a644c1cd9770b2d..4a0df1b229b2c4961595bc2f72c50b4f4b8dc440 100644 --- a/src/apps/rednatco/idents.ts +++ b/src/apps/rednatco/idents.ts @@ -18,9 +18,10 @@ export function ID(id: ID, sub: Substructure|'', ref: string) { } export type DensityID = 'data'|'volume'|'visual'; +// export type DensityKind = 'absolute'|'difference-positive'|'difference-negative'; -export function DensityID(id: DensityID, ref: string) { - return `${ref}_density-map_${id}`; +export function DensityID(index: number, id: DensityID, ref: string) { + return `${ref}_density-map_${index}_${id}`; } export function isVisual(ident: string) { diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx index f341c19528a695b085896acc37a901899189cde4..010f6db2b581f98e1e82a9b0eb16d3076734416a 100644 --- a/src/apps/rednatco/index.tsx +++ b/src/apps/rednatco/index.tsx @@ -7,8 +7,8 @@ import { Filters } from './filters'; import { ReDNATCOMspViewer } from './viewer'; import { NtCColors } from './colors'; import { ColorPicker } from './color-picker'; -import { CollapsibleVertical, PushButton, ToggleButton } from './controls'; -import { isoBounds, luminance, toggleArray } from './util'; +import { CollapsibleVertical, ColorBox, PushButton, ToggleButton } from './controls'; +import { toggleArray } from './util'; import { Color } from '../../mol-util/color'; import { assertUnreachable } from '../../mol-util/type-helpers'; import './index.html'; @@ -27,10 +27,24 @@ const ConformersByClass = { type ConformersByClass = typeof ConformersByClass; const DefaultChainColor = Color(0xD9D9D9); -const DefaultDensityMapAlpha = 0.5; +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 const DefaultDensityDifferencePositiveColor = Color(0x00C300); +export const DefaultDensityDifferenceNegativeColor = Color(0xC30000); +export const DefaultDensityMapColor = Color(0x009DFF); +const DefaultDensityMapDisplay = { + kind: '2fo-fc' as DensityMapKind, + representations: ['solid'] as DensityMapRepresentation[], + isoValue: 0, + + alpha: DefaultDensityMapAlpha, + colors: [{ color: DefaultDensityMapColor, name: 'Color' }], +}; +export type DensityMapDisplay = typeof DefaultDensityMapDisplay; const Display = { structures: { @@ -53,13 +67,7 @@ const Display = { chainColor: DefaultChainColor, waterColor: DefaultWaterColor, }, - densityMap: { - representations: ['wireframe'] as DensityMapRepresentation[], - isoValue: 0, - - alpha: DefaultDensityMapAlpha, - color: 0x000000 - }, + densityMaps: [] as DensityMapDisplay[], }; export type Display = typeof Display; @@ -69,20 +77,6 @@ function capitalize(s: string) { return s[0].toLocaleUpperCase() + s.slice(1); } -class ColorBox extends React.Component<{ caption: string, color: Color }> { - render() { - const lum = luminance(this.props.color); - return ( - <div - className='rmsp-color-box' - style={{ backgroundColor: Color.toStyle(this.props.color) }} - > - <span style={{ color: lum > 0.6 ? 'black' : 'white' }}>{this.props.caption}</span> - </div> - ); - } -} - interface State { display: Display; showControls: boolean; @@ -231,11 +225,32 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { } } - loadStructure(coords: { data: string, type: 'pdb'|'cif' }, densityMap: { data: Uint8Array, type: 'ccp4'|'dsn6'|'ds' }|null) { + loadStructure(coords: { data: string, type: 'pdb'|'cif' }, densityMaps: { data: Uint8Array, type: 'ccp4'|'dsn6', kind: '2fo-fc'|'fo-fc'|'em' }[]|null) { if (this.viewer) { - this.viewer.loadStructure(coords, densityMap, this.state.display).then(() => { + const display = { ...this.state.display }; + if (densityMaps) { + display.densityMaps.length = densityMaps.length; + for (let idx = 0; idx < densityMaps.length; idx++) { + const dm = densityMaps[idx]; + + if (dm.kind === 'fo-fc') { + display.densityMaps[idx] = { + ...DefaultDensityMapDisplay, + kind: dm.kind, + colors: [ + { color: DefaultDensityDifferencePositiveColor, name: '+ color' }, + { color: DefaultDensityDifferenceNegativeColor, name: '- color' }, + ], + }; + } else + display.densityMaps[idx] = { ...DefaultDensityMapDisplay, kind: dm.kind }; + } + } else + display.densityMaps.length = 0; + + this.viewer.loadStructure(coords, densityMaps, display).then(() => { this.presentConformers = this.viewer!.getPresentConformers(); - this.forceUpdate(); + this.setState({ ...this.state, display }); ReDNATCOMspApi.event(Api.Events.StructureLoaded()); }); } @@ -271,9 +286,6 @@ 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> @@ -500,44 +512,44 @@ export class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { </div> </div> </CollapsibleVertical> - {this.viewer?.hasDensityMap() + {this.viewer?.hasDensityMaps() ? <DensityMapControls - wireframe={this.state.display.densityMap.representations.includes('wireframe')} - toggleWireframe={() => { + viewer={this.viewer} + display={this.state.display.densityMaps} + toggleWireframe={(index) => { const display = { ...this.state.display }; - display.densityMap.representations = toggleArray(display.densityMap.representations, 'wireframe'); + display.densityMaps[index].representations = toggleArray(display.densityMaps[index].representations, 'wireframe'); - this.viewer!.changeDensityMap(display); + this.viewer!.changeDensityMap(index, display); this.setState({ ...this.state, display }); }} - solid={this.state.display.densityMap.representations.includes('solid')} - toggleSolid={() => { + toggleSolid={(index) => { const display = { ...this.state.display }; - display.densityMap.representations = toggleArray(display.densityMap.representations, 'solid'); + display.densityMaps[index].representations = toggleArray(display.densityMaps[index].representations, 'solid'); - this.viewer!.changeDensityMap(display); + this.viewer!.changeDensityMap(index, display); this.setState({ ...this.state, display }); }} - - isoMin={_isoBounds.min} - isoMax={_isoBounds.max} - isoStep={_isoBounds.step} - iso={this.state.display.densityMap.isoValue} - changeIso={(v) => { + changeIso={(index, v) => { const display = { ...this.state.display }; - display.densityMap.isoValue = v; + display.densityMaps[index].isoValue = v; - this.viewer!.changeDensityMap(display); + this.viewer!.changeDensityMap(index, display); this.setState({ ...this.state, display }); }} + changeAlpha={(index, alpha) => { + const display = { ...this.state.display }; + display.densityMaps[index].alpha = alpha; - alpha={this.state.display.densityMap.alpha} - changeAlpha={(alpha) => { + this.viewer!.changeDensityMap(index, display); + this.setState({ ...this.state, display }); + }} + changeColors={(index, colors) => { const display = { ...this.state.display }; - display.densityMap.alpha = alpha; + display.densityMaps[index].colors = colors; - this.viewer!.changeDensityMap(display); + this.viewer!.changeDensityMap(index, display); this.setState({ ...this.state, display }); }} /> diff --git a/src/apps/rednatco/rednatco-molstar.css b/src/apps/rednatco/rednatco-molstar.css index fddb76fdec5905731c5c7e0360845e52e3139919..5f33a4a53ebbb98fa22f0b20617dbd0a52db377a 100644 --- a/src/apps/rednatco/rednatco-molstar.css +++ b/src/apps/rednatco/rednatco-molstar.css @@ -79,6 +79,9 @@ flex: 1; } +.rmsp-control-item-squished { +} + .rmsp-controls-line { align-items: center; display: flex; diff --git a/src/apps/rednatco/viewer.ts b/src/apps/rednatco/viewer.ts index aacf7a673e73650f3714bce1dc6b8f57325c09e2..a6c978d72a9b6ddd114f4e0b338ecce9b6976157 100644 --- a/src/apps/rednatco/viewer.ts +++ b/src/apps/rednatco/viewer.ts @@ -244,20 +244,28 @@ export class ReDNATCOMspViewer { this.app = app; } - private densityMapVisuals(vis: Display['densityMap']) { + private densityMapVisuals(vis: Display['densityMaps'][0], visKind: 'absolute'|'positive'|'negative') { + const isoValue = visKind === 'absolute' + ? Volume.IsoValue.absolute(vis.isoValue) + : visKind === 'positive' + ? Volume.IsoValue.relative(vis.isoValue) : Volume.IsoValue.relative(-vis.isoValue); + + const color = visKind === 'absolute' || visKind === 'positive' + ? vis.colors[0] : vis.colors[1] + return { type: { name: 'isosurface', params: { alpha: vis.alpha, - isoValue: Volume.IsoValue.absolute(vis.isoValue), + isoValue, visuals: vis.representations, sizeFactor: 0.75, } }, colorTheme: { name: 'uniform', - params: { value: Color(vis.color) }, + params: { value: Color(color.color) }, }, }; } @@ -649,22 +657,41 @@ export class ReDNATCOMspViewer { await b.commit(); } - async changeDensityMap(display: Display) { - if (!this.hasDensityMap()) + async changeDensityMap(index: number, display: Display) { + if (!this.hasDensityMaps()) return; - const b = this.plugin.state.data.build().to(IDs.DensityID('visual', BaseRef)); - const vis = display.densityMap; + const dm = display.densityMaps[index]; - b.update( - StateTransforms.Representation.VolumeRepresentation3D, - old => ({ - ...old, - ...this.densityMapVisuals(vis), - }) - ); - - await b.commit(); + if (dm.kind === 'fo-fc') { + await this.plugin.state.data.build().to(IDs.DensityID(index, 'visual', BaseRef + '_pos')) + .update( + StateTransforms.Representation.VolumeRepresentation3D, + old => ({ + ...old, + ...this.densityMapVisuals(dm, 'positive'), + }) + ) + .to(IDs.DensityID(index, 'visual', BaseRef + '_neg')) + .update( + StateTransforms.Representation.VolumeRepresentation3D, + old => ({ + ...old, + ...this.densityMapVisuals(dm, 'negative'), + }) + ) + .commit(); + } else { + await this.plugin.state.data.build().to(IDs.DensityID(index, 'visual', BaseRef)) + .update( + StateTransforms.Representation.VolumeRepresentation3D, + old => ({ + ...old, + ...this.densityMapVisuals(dm, 'absolute'), + }) + ) + .commit(); + } } currentModelNumber() { @@ -674,8 +701,8 @@ 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)); + densityMapIsoRange(index: number, ref = BaseRef): { min: number, max: number }|undefined { + const cell = this.plugin.state.data.cells.get(IDs.DensityID(index, 'volume', ref)); if (!cell || !cell.obj) return void 0; @@ -842,8 +869,8 @@ 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; + hasDensityMaps(ref = BaseRef) { + return !!this.plugin.state.data.cells.get(IDs.DensityID(0, 'volume', ref))?.obj?.data; } isReady() { @@ -852,7 +879,7 @@ export class ReDNATCOMspViewer { async loadStructure( coords: { data: string, type: 'pdb'|'cif' }, - densityMap: { data: Uint8Array, type: 'ccp4'|'dsn6'|'ds' }|null, + densityMaps: { data: Uint8Array, type: 'ccp4'|'dsn6', kind: '2fo-fc'|'fo-fc'|'em' }[]|null, display: Display ) { // TODO: Remove the currently loaded structure @@ -941,42 +968,54 @@ export class ReDNATCOMspViewer { await b3.commit(); // Load density map, if any - if (densityMap) { - // This is ridiculous but anything saner breaks type checker - if (densityMap.type === 'ccp4') { - await this.plugin.state.data.build().toRoot() - .apply(RawData, { data: densityMap.data }, { ref: IDs.DensityID('data', BaseRef) }) - .apply(StateTransforms.Data.ParseCcp4) - .apply(StateTransforms.Volume.VolumeFromCcp4, {}, { ref: IDs.DensityID('volume', BaseRef) }) - .commit(); - } else if (densityMap.type === 'dsn6') { - await this.plugin.state.data.build().toRoot() - .apply(RawData, { data: densityMap.data }, { ref: IDs.DensityID('data', BaseRef) }) - .apply(StateTransforms.Data.ParseDsn6) - .apply(StateTransforms.Volume.VolumeFromDsn6, {}, { ref: IDs.DensityID('volume', BaseRef) }) - .commit(); - } else if (densityMap.type === 'ds') { - await this.plugin.state.data.build().toRoot() - .apply(RawData, { data: densityMap.data }, { ref: IDs.DensityID('data', BaseRef) }) - .apply(StateTransforms.Data.ParseCif) - .apply(StateTransforms.Volume.VolumeFromDensityServerCif, {}, { ref: IDs.DensityID('volume', BaseRef) }) - .commit(); - } - - const isoRange = this.densityMapIsoRange()!; - const bounds = isoBounds(isoRange.min, isoRange.max); - const iso = prettyIso(((isoRange.max - isoRange.min) / 2) + isoRange.min, bounds.step); - - display.densityMap.representations = ['wireframe']; - display.densityMap.isoValue = iso; + if (densityMaps) { + for (let idx = 0; idx < densityMaps.length; idx++) { + const dm = densityMaps[idx]; + if (dm.type === 'ccp4') { + await this.plugin.state.data.build().toRoot() + .apply(RawData, { data: dm.data }, { ref: IDs.DensityID(idx, 'data', BaseRef) }) + .apply(StateTransforms.Data.ParseCcp4) + .apply(StateTransforms.Volume.VolumeFromCcp4, {}, { ref: IDs.DensityID(idx, 'volume', BaseRef) }) + .commit(); + } else if (dm.type === 'dsn6') { + await this.plugin.state.data.build().toRoot() + .apply(RawData, { data: dm.data }, { ref: IDs.DensityID(idx, 'data', BaseRef) }) + .apply(StateTransforms.Data.ParseDsn6) + .apply(StateTransforms.Volume.VolumeFromDsn6, {}, { ref: IDs.DensityID(idx, 'volume', BaseRef) }) + .commit(); + } - await this.plugin.state.data.build().to(IDs.DensityID('volume', BaseRef)) - .apply( - StateTransforms.Representation.VolumeRepresentation3D, - this.densityMapVisuals(display.densityMap), - { ref: IDs.DensityID('visual', BaseRef) } - ) - .commit(); + const isoRange = this.densityMapIsoRange(idx)!; + const bounds = isoBounds(isoRange.min, isoRange.max); + + if (dm.kind === 'fo-fc') { + display.densityMaps[idx].isoValue = prettyIso(isoRange.max * 0.67, bounds.step); + + this.plugin.state.data.build().to(IDs.DensityID(idx, 'volume', BaseRef)) + .apply( + StateTransforms.Representation.VolumeRepresentation3D, + this.densityMapVisuals(display.densityMaps[idx], 'positive'), + { ref: IDs.DensityID(idx, 'visual', BaseRef + '_pos') } + ) + .to(IDs.DensityID(idx, 'volume', BaseRef)) + .apply( + StateTransforms.Representation.VolumeRepresentation3D, + this.densityMapVisuals(display.densityMaps[idx], 'negative'), + { ref: IDs.DensityID(idx, 'visual', BaseRef + '_neg') } + ) + .commit(); + } else { + display.densityMaps[idx].isoValue = prettyIso(((isoRange.max - isoRange.min) / 2) + isoRange.min, bounds.step); + + await this.plugin.state.data.build().to(IDs.DensityID(idx, 'volume', BaseRef)) + .apply( + StateTransforms.Representation.VolumeRepresentation3D, + this.densityMapVisuals(display.densityMaps[idx], 'absolute'), + { ref: IDs.DensityID(idx, 'visual', BaseRef) } + ) + .commit(); + } + } } this.haveMultipleModels = this.getModelCount() > 1;