Skip to content
Snippets Groups Projects
Commit c6ff8e6f authored by Michal Malý's avatar Michal Malý
Browse files

ReDNATCO plugin stage 2

parent 2b77f6d2
No related branches found
No related tags found
No related merge requests found
export type ID ='data'|'structure'|'visual'|'pyramids'; export type ID ='data'|'structure'|'visual'|'pyramids';
export type Substructure = 'nucleic'|'protein'|'water';
export function ID(id: ID, ref: string) { export function ID(id: ID, sub: Substructure|'', ref: string) {
return `${id}_${ref}`; if (sub === '')
return `${id}_${ref}`;
return `${id}_${sub}_${ref}`;
} }
...@@ -4,8 +4,18 @@ ...@@ -4,8 +4,18 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="molstar.css" /> <link rel="stylesheet" type="text/css" href="molstar.css" />
</head> </head>
<body> <body style="height: 100vh; width: 100hw">
<div id="app"></div> <div id="xxx-app" style="height: 100%; width: 100%"></div>
<script type="text/javascript" src="./molstar.js"></script> <script type="text/javascript" src="./molstar.js"></script>
<script>
async function loadStructure() {
const resp = await fetch('./3vok_v32C35A23.cif');
const data = await resp.text();
molstar.ReDNATCOMspApi.loadStructure(data);
}
molstar.ReDNATCOMspApi.init('xxx-app', loadStructure);
</script>
</body> </body>
</html> </html>
...@@ -29,6 +29,8 @@ import { MarkerAction } from '../../mol-util/marker-action'; ...@@ -29,6 +29,8 @@ import { MarkerAction } from '../../mol-util/marker-action';
import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ObjectKeys } from '../../mol-util/type-helpers'; import { ObjectKeys } from '../../mol-util/type-helpers';
import './index.html'; import './index.html';
import './molstar.css';
import './rednatco-molstar.css';
const Extensions = { const Extensions = {
'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids), 'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
...@@ -37,6 +39,52 @@ const Extensions = { ...@@ -37,6 +39,52 @@ const Extensions = {
const BaseRef = 'rdo'; const BaseRef = 'rdo';
const AnimationDurationMsec = 150; const AnimationDurationMsec = 150;
function capitalize(s: string) {
if (s.length === 0)
return s;
return s[0].toLocaleUpperCase() + s.slice(1);
}
class PushButton extends React.Component<{ caption: string, enabled: boolean, onClick: () => void }> {
render() {
return (
<div
className={`rmsp-pushbutton ${this.props.enabled ? '' : 'rmsp-pushbutton-disabled'}`}
onClick={() => this.props.enabled ? this.props.onClick() : {}}
>
<div className={`${this.props.enabled ? 'rmsp-pushbutton-text' : 'rmsp-pushbutton-text-disabled'}`}>{this.props.caption}</div>
</div>
);
}
}
class ToggleButton extends React.Component<{ caption: string, enabled: boolean, switchedOn: boolean, onClick: () => void }> {
render() {
return (
<div
className={`rmsp-pushbutton ${this.props.enabled ? (this.props.switchedOn ? 'rmsp-togglebutton-switched-on' : 'rmsp-togglebutton-switched-off') : 'rmsp-pushbutton-disabled'}`}
onClick={() => this.props.enabled ? this.props.onClick() : {}}
>
<div className={`${this.props.enabled ? 'rmsp-pushbutton-text' : 'rmsp-pushbutton-text-disabled'}`}>{this.props.caption}</div>
</div>
);
}
}
const Display = {
representation: 'cartoon',
showNucleic: true,
showProtein: false,
showWater: false,
showPyramids: true,
modelNumber: 1,
};
type Display = typeof Display;
const ReDNATCOLociLabelProvider = PluginBehavior.create({ const ReDNATCOLociLabelProvider = PluginBehavior.create({
name: 'watlas-loci-label-provider', name: 'watlas-loci-label-provider',
category: 'interaction', category: 'interaction',
...@@ -176,6 +224,10 @@ class ReDNATCOMspViewer { ...@@ -176,6 +224,10 @@ class ReDNATCOMspViewer {
constructor(public plugin: PluginUIContext) { constructor(public plugin: PluginUIContext) {
} }
private getBuilder(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
return this.plugin.state.data.build().to(IDs.ID(id, sub, ref));
}
private pyramidsParams(colors: Map<string, Color>, visible: Map<string, boolean>, transparent: boolean) { private pyramidsParams(colors: Map<string, Color>, visible: Map<string, boolean>, transparent: boolean) {
const typeParams = {} as PD.Values<ConfalPyramidsParams>; const typeParams = {} as PD.Values<ConfalPyramidsParams>;
for (const k of Reflect.ownKeys(ConfalPyramidsParams) as (keyof ConfalPyramidsParams)[]) { for (const k of Reflect.ownKeys(ConfalPyramidsParams) as (keyof ConfalPyramidsParams)[]) {
...@@ -229,39 +281,151 @@ class ReDNATCOMspViewer { ...@@ -229,39 +281,151 @@ class ReDNATCOMspViewer {
return new ReDNATCOMspViewer(plugin); return new ReDNATCOMspViewer(plugin);
} }
async loadStructure(data: string, type: 'pdb'|'cif') { async changeRepresentation(display: Partial<Display>) {
const b = this.plugin.state.data.build();
const repr = display.representation ?? 'cartoon';
for (const sub of ['nucleic', 'protein', 'water'] as IDs.Substructure[]) {
if (this.has('visual', sub)) {
b.to(IDs.ID('visual', sub, BaseRef))
.update(
StateTransforms.Representation.StructureRepresentation3D,
old => ({
...old,
type: { ...old.type, name: repr }
})
);
}
}
await b.commit();
}
has(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj;
}
async loadStructure(data: string, type: 'pdb'|'cif', display: Partial<Display>) {
await this.plugin.state.data.build().toRoot().commit(); await this.plugin.state.data.build().toRoot().commit();
const b = (t => type === 'pdb' const b = (t => type === 'pdb'
? t.apply(StateTransforms.Model.TrajectoryFromPDB) ? t.apply(StateTransforms.Model.TrajectoryFromPDB)
: t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) : t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
)(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', BaseRef) })) )(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', '', BaseRef) }))
.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
.apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', BaseRef) }) .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', BaseRef) })
.apply( // Extract substructures
StateTransforms.Representation.StructureRepresentation3D, .apply(StateTransforms.Model.StructureComplexElement, { type: 'nucleic' }, { ref: IDs.ID('structure', 'nucleic', BaseRef) })
{ .to(IDs.ID('structure', '', BaseRef))
type: { name: 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } }, .apply(StateTransforms.Model.StructureComplexElement, { type: 'protein' }, { ref: IDs.ID('structure', 'protein', BaseRef) })
}, .to(IDs.ID('structure', '', BaseRef))
{ ref: IDs.ID('visual', BaseRef) } .apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: IDs.ID('structure', 'water', BaseRef) });
) // Commit now so that we can check whether individual substructures are available
.to(IDs.ID('structure', BaseRef)) await b.commit();
.apply(
StateTransforms.Representation.StructureRepresentation3D, // Create default visuals
this.pyramidsParams(new Map(), new Map(), false), const bb = this.plugin.state.data.build();
{ ref: IDs.ID('pyramids', BaseRef) } if (display.showNucleic && this.has('structure', 'nucleic')) {
); bb.to(IDs.ID('structure', 'nucleic', BaseRef))
.apply(
b.commit(); StateTransforms.Representation.StructureRepresentation3D,
{
type: { name: display.representation ?? 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } },
},
{ ref: IDs.ID('visual', 'nucleic', BaseRef) }
);
if (display.showPyramids) {
bb.to(IDs.ID('structure', 'nucleic', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
this.pyramidsParams(new Map(), new Map(), false),
{ ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
);
}
}
if (display.showProtein && this.has('structure', 'protein')) {
bb.to(IDs.ID('structure', 'protein', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
{
type: { name: display.representation ?? 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } },
},
{ ref: IDs.ID('visual', 'protein', BaseRef) }
);
}
if (display.showWater && this.has('structure', 'water')) {
bb.to(IDs.ID('structure', 'water', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
{
type: { name: display.representation ?? 'ball-and-stick', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35 } },
},
{ ref: IDs.ID('visual', 'water', BaseRef) }
);
}
await bb.commit();
}
isReady() {
return this.has('structure', '', BaseRef);
}
async togglePyramids(display: Partial<Display>) {
if (display.showPyramids && !this.has('pyramids', 'nucleic')) {
const b = this.getBuilder('structure', 'nucleic');
if (b) {
b.apply(
StateTransforms.Representation.StructureRepresentation3D,
this.pyramidsParams(new Map(), new Map(), false),
{ ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
);
await b.commit();
}
} else {
await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('pyramids', 'nucleic', BaseRef) });
}
}
async toggleSubstructure(sub: IDs.Substructure, display: Partial<Display>) {
const show = sub === 'nucleic' ? !!display.showNucleic :
sub === 'protein' ? !!display.showProtein : !!display.showWater;
const repr = display.representation ?? 'cartoon';
if (show) {
const b = this.getBuilder('structure', sub);
if (b) {
b.apply(
StateTransforms.Representation.StructureRepresentation3D,
{
type: { name: repr, params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } }, // TODO: Use different params for water
},
{ ref: IDs.ID('visual', sub, BaseRef) }
);
await b.commit();
}
} else
await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) });
} }
} }
class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props> { interface State {
display: Display;
}
class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
private viewer: ReDNATCOMspViewer|null = null; private viewer: ReDNATCOMspViewer|null = null;
constructor(props: ReDNATCOMsp.Props) {
super(props);
this.state = {
display: { ...Display },
};
}
loadStructure(data: string, type: 'pdb'|'cif') { loadStructure(data: string, type: 'pdb'|'cif') {
if (this.viewer) if (this.viewer)
this.viewer.loadStructure(data, type); this.viewer.loadStructure(data, type, this.state.display).then(() => this.forceUpdate());
} }
componentDidMount() { componentDidMount() {
...@@ -278,10 +442,113 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props> { ...@@ -278,10 +442,113 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props> {
} }
render() { render() {
const ready = this.viewer?.isReady() ?? false;
const hasNucleic = this.viewer?.has('structure', 'nucleic') ?? false;
const hasProtein = this.viewer?.has('structure', 'protein') ?? false;
const hasWater = this.viewer?.has('structure', 'water') ?? false;
return ( return (
<div className='rmsp-app'> <div className='rmsp-app'>
<div id={this.props.elemId + '-viewer'}></div> <div id={this.props.elemId + '-viewer'} className='rmsp-viewer'></div>
<div>Controls</div> <div>
<div>Display and control</div>
<div className='rmsp-controls'>
<div className='rmsp-controls-section-caption'>Representation</div>
<div className='rmsp-controls-line'>
<div className='rmsp-control-item'>
<PushButton
caption={capitalize(this.state.display.representation)}
enabled={ready}
onClick={() => {
const display = {
...this.state.display,
representation: this.state.display.representation === 'cartoon' ? 'ball-and-stick' : 'cartoon',
};
this.viewer!.changeRepresentation(display);
this.setState({ ...this.state, display });
}}
/>
</div>
</div>
<div className='rmsp-controls-section-caption'>Substructure parts</div>
<div className='rmsp-controls-line'>
<div className='rmsp-control-item'>
<ToggleButton
caption='Nucleic'
enabled={hasNucleic}
switchedOn={this.state.display.showNucleic}
onClick={() => {
const display = {
...this.state.display,
showNucleic: !this.state.display.showNucleic,
};
this.viewer!.toggleSubstructure('nucleic', display);
this.setState({ ...this.state, display });
}}
/>
</div>
<div className='rmsp-control-item'>
<ToggleButton
caption='Protein'
enabled={hasProtein}
switchedOn={this.state.display.showProtein}
onClick={() => {
const display = {
...this.state.display,
showProtein: !this.state.display.showProtein,
};
this.viewer!.toggleSubstructure('protein', display);
this.setState({ ...this.state, display });
}}
/>
</div>
<div className='rmsp-control-item'>
<ToggleButton
caption='Water'
enabled={hasWater}
switchedOn={this.state.display.showWater}
onClick={() => {
const display = {
...this.state.display,
showWater: !this.state.display.showWater,
};
this.viewer!.toggleSubstructure('water', display);
this.setState({ ...this.state, display });
}}
/>
</div>
</div>
<div className='rmsp-controls-section-caption'>NtC visuals</div>
<div className='rmsp-controls-line'>
<div className='rmsp-control-item'>
<ToggleButton
caption='Pyramids'
enabled={ready}
switchedOn={this.state.display.showPyramids}
onClick={() => {
const display = {
...this.state.display,
pyramidsShown: !this.state.display.showPyramids,
};
this.viewer!.togglePyramids(display);
this.setState({ ...this.state, display });
}}
/>
</div>
<div className='rmsp-control-item'>
<ToggleButton
caption='Balls'
enabled={false}
switchedOn={false}
onClick={() => {}}
/>
</div>
</div>
</div>
</div>
</div> </div>
); );
} }
...@@ -325,4 +592,3 @@ class _ReDNATCOMspApi { ...@@ -325,4 +592,3 @@ class _ReDNATCOMspApi {
} }
export const ReDNATCOMspApi = new _ReDNATCOMspApi(); export const ReDNATCOMspApi = new _ReDNATCOMspApi();
This diff is collapsed.
/* vim: set sw=4 tw=4 sts=4 expandtab : */
:root {
--anim-speed: 200ms;
--h-gap: 1em;
--v-gap: 0.5em;
--x-gap: 1em;
--color-a: #aaa;
--color-b: #ccc;
--color-c: #f0f0f0;
--color-d: #4caf50;
--color-e: #265b6a;
--color-f: grey;
--color-x: #9e9e9e;
--color-y: #616161;
--font-small: 10pt;
--font-large: 14pt;
--font-xxlarge: 22pt;
--font-mega: 32pt;
--font-uber: 48pt;
--thickness-border: 2px;
}
.rmsp-app {
display: flex;
flex-direction: column;
height: 90%;
width: 100%;
font-family: Verdana, sans-serif;
font-size: 12pt;
line-height: 1.5;
}
.rmsp-controls {
display: grid;
grid-template-columns: auto 1fr;
grid-column-gap: var(--h-gap);
overflow: scroll;
}
.rmsp-control-item {
flex: 1;
}
.rmsp-controls-line {
align-items: center;
display: flex;
gap: var(--h-gap);
}
.rmsp-controls-section-caption {
font-weight: bold;
}
.rmsp-pushbutton {
align-items: center;
display: flex;
justify-content: center;
transition: background-color var(--anim-speed);
}
.rmsp-pushbutton:hover {
background-color: var(--color-b);
}
.rmsp-pushbutton-disabled {
background-color: var(--color-a);
}
.rmsp-togglebutton-switched-off {
background-color: red;
}
.rmsp-togglebutton-switched-on {
background-color: green;
}
.rmsp-pushbutton-text {
font-weight: bold;
margin: 0.15em;
}
.rmsp-pushbutton-text-disabled {
color: var(--color-a);
font-weight: bold;
margin: 0.15em;
}
.rmsp-viewer {
flex: 1;
position: relative;
}
...@@ -40,6 +40,7 @@ const sharedConfig = { ...@@ -40,6 +40,7 @@ const sharedConfig = {
plugins: [ plugins: [
new ExtraWatchWebpackPlugin({ new ExtraWatchWebpackPlugin({
files: [ files: [
'./lib/**/*.css',
'./lib/**/*.scss', './lib/**/*.scss',
'./lib/**/*.html' './lib/**/*.html'
], ],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment