ソースを参照

added draft of ParamMapping interface and control

David Sehnal 5 年 前
コミット
11ed0ca89b

+ 5 - 0
package-lock.json

@@ -8983,6 +8983,11 @@
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
       "dev": true
     },
+    "immer": {
+      "version": "5.3.6",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-5.3.6.tgz",
+      "integrity": "sha512-pqWQ6ozVfNOUDjrLfm4Pt7q4Q12cGw2HUZgry4Q5+Myxu9nmHRkWBpI0J4+MK0AxbdFtdMTwEGVl7Vd+vEiK+A=="
+    },
     "immutable": {
       "version": "3.8.2",
       "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",

+ 1 - 0
package.json

@@ -116,6 +116,7 @@
     "cors": "^2.8.5",
     "express": "^4.17.1",
     "graphql": "^14.6.0",
+    "immer": "^5.3.6",
     "immutable": "^3.8.2",
     "node-fetch": "^2.6.0",
     "react": "^16.12.0",

+ 28 - 3
src/mol-plugin-ui/controls/parameters.tsx

@@ -15,21 +15,25 @@ import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
 import { NumericInput, IconButton, ControlGroup } from './common';
-import { _Props, _State } from '../base';
+import { _Props, _State, PluginUIComponent } from '../base';
 import { legendFor } from './legend';
 import { Legend as LegendData } from '../../mol-util/legend';
 import { CombinedColorControl, ColorValueOption, ColorOptions } from './color';
 import { getPrecision } from '../../mol-util/number';
+import { ParamMapping } from '../../mol-util/param-mapping';
+import { PluginContext } from '../../mol-plugin/context';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
-    onChange: ParamOnChange,
+    onChange: ParamsOnChange<PD.Values<P>>,
     isDisabled?: boolean,
     onEnter?: () => void
 }
 
 export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
+    onChange: ParamOnChange = (params) => this.props.onChange(params, this.props.values);
+
     render() {
         const params = this.props.params;
         const values = this.props.values;
@@ -41,12 +45,32 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
                 if (param.isHidden) return null;
                 const Control = controlFor(param);
                 if (!Control) return null;
-                return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
+                return <Control param={param} key={key} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
             })}
         </>;
     }
 }
 
+export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: ParamMapping<S, T, PluginContext> }> {
+    setSettings = (p: { param: PD.Base<any>, name: string, value: any }, old: any) => {
+        const values = { ...old, [p.name]: p.value };
+        const t = this.props.mapping.update(values, this.plugin);
+        this.props.mapping.apply(t, this.plugin);
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    render() {
+        const t = this.props.mapping.getTarget(this.plugin);
+        const values = this.props.mapping.getValues(t, this.plugin);
+        const params = this.props.mapping.params(this.plugin) as any as PD.Params;
+        return <ParameterControls params={params} values={values} onChange={this.setSettings} />
+    }
+}
+
+
 function controlFor(param: PD.Any): ParamControl | undefined {
     switch (param.type) {
         case 'value': return void 0;
@@ -91,6 +115,7 @@ export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legen
     }
 }
 
+export type ParamsOnChange<P> = (params: { param: PD.Base<any>, name: string, value: any }, values: Readonly<P>) => void
 export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
 export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
     name: string,

+ 55 - 90
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -9,10 +9,23 @@ import * as React from 'react';
 import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { PluginCommands } from '../../mol-plugin/command';
 import { ColorNames } from '../../mol-util/color/names';
-import { ParameterControls } from '../controls/parameters';
+import { ParameterMappingControl } from '../controls/parameters';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginUIComponent } from '../base';
 import { Color } from '../../mol-util/color';
+import { ParamMapping } from '../../mol-util/param-mapping';
+import { PluginContext } from '../../mol-plugin/context';
+
+export class SimpleSettingsControl extends PluginUIComponent {
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    render() {
+        if (!this.plugin.canvas3d) return null;
+        return <ParameterMappingControl mapping={SimpleSettingsMapping} />
+    }
+}
 
 const SimpleSettingsParams = {
     spin: Canvas3DParams.trackball.params.spin,
@@ -21,80 +34,21 @@ const SimpleSettingsParams = {
         'transparent': PD.EmptyGroup(),
         'opaque': PD.Group({ color: PD.Color(Color(0xFCFBF9), { description: 'Custom background color' }) }, { isFlat: true })
     }, { description: 'Background of the 3D canvas' }),
-    renderStyle: PD.Select('glossy', [['flat', 'Flat'], ['matte', 'Matte'], ['glossy', 'Glossy'], ['metallic', 'Metallic']], { description: 'Style in which the 3D scene is rendered' }),
+    renderStyle: PD.Select('glossy', PD.arrayToOptions(['flat', 'matte', 'glossy', 'metallic']), { description: 'Style in which the 3D scene is rendered' }),
     occlusion: PD.Boolean(false, { description: 'Darken occluded crevices with the ambient occlusion effect' }),
     outline: PD.Boolean(false, { description: 'Draw outline around 3D objects' }),
     fog: PD.Boolean(false, { description: 'Show fog in the distance' }),
     clipFar: PD.Boolean(true, { description: 'Clip scene in the distance' }),
 };
 
-export class SimpleSettingsControl extends PluginUIComponent {
-    setSettings = (p: { param: PD.Base<any>, name: keyof typeof SimpleSettingsParams | string, value: any }) => {
-        if (p.name === 'spin') {
-            if (!this.plugin.canvas3d) return;
-            const trackball = this.plugin.canvas3d.props.trackball;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: p.value } } });
-        } else if (p.name === 'camera') {
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { cameraMode: p.value }});
-        } else if (p.name === 'background') {
-            if (!this.plugin.canvas3d) return;
-            const renderer = this.plugin.canvas3d.props.renderer;
-            const color: typeof SimpleSettingsParams['background']['defaultValue'] = p.value;
-            if (color.name === 'transparent') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: ColorNames.white }, transparentBackground: true } });
-            } else {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: color.params.color }, transparentBackground: false } });
-            }
-        } else if (p.name === 'renderStyle') {
-            if (!this.plugin.canvas3d) return;
-
-            const renderer = this.plugin.canvas3d.props.renderer;
-            if (p.value === 'flat') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'matte') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }
-                } });
-            } else if (p.value === 'glossy') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'metallic') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }
-                } });
-            }
-        } else if (p.name === 'occlusion') {
-            if (!this.plugin.canvas3d) return;
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, occlusionEnable: p.value, occlusionBias: 0.5, occlusionRadius: 64 },
-            } });
-        } else if (p.name === 'outline') {
-            if (!this.plugin.canvas3d) return;
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, outlineEnable: p.value },
-            } });
-        } else if (p.name === 'fog') {;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                cameraFog: p.value ? 50 : 0,
-            } });
-        } else if (p.name === 'clipFar') {;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                cameraClipFar: p.value,
-            } });
-        }
-    }
-
-    get values () {
-        const renderer = this.plugin.canvas3d?.props.renderer;
-
-        let renderStyle = 'custom'
-        let background: typeof SimpleSettingsParams['background']['defaultValue'] = { name: 'transparent', params: { } }
+type SimpleSettingsParams = typeof SimpleSettingsParams
+const SimpleSettingsMapping = ParamMapping({
+    params: SimpleSettingsParams,
+    target(ctx: PluginContext) { return ctx.canvas3d?.props!; } })({
+    values(t, ctx) {
+        const renderer = t.renderer;
 
+        let renderStyle: SimpleSettingsParams['renderStyle']['defaultValue'] = 'custom' as any;
         if (renderer) {
             if (renderer.lightIntensity === 0 && renderer.ambientIntensity === 1 && renderer.roughness === 0.4 && renderer.metalness === 0) {
                 renderStyle = 'flat'
@@ -107,31 +61,42 @@ export class SimpleSettingsControl extends PluginUIComponent {
                     renderStyle = 'metallic'
                 }
             }
-
-            if (renderer.backgroundColor === ColorNames.white && this.plugin.canvas3d?.props.transparentBackground) {
-                background = { name: 'transparent', params: { } }
-            } else {
-                background = { name: 'opaque', params: { color: renderer.backgroundColor } }
-            }
         }
 
         return {
-            spin: !!this.plugin.canvas3d?.props.trackball.spin,
-            camera: this.plugin.canvas3d?.props.cameraMode,
-            background,
+            spin: !!t.trackball.spin,
+            camera: t.cameraMode,
+            background:  (renderer.backgroundColor === ColorNames.white && t.transparentBackground) 
+                ? { name: 'transparent', params: { } }
+                : { name: 'opaque', params: { color: renderer.backgroundColor } },
             renderStyle,
-            occlusion: this.plugin.canvas3d?.props.postprocessing.occlusionEnable,
-            outline: this.plugin.canvas3d?.props.postprocessing.outlineEnable,
-            fog: this.plugin.canvas3d ? this.plugin.canvas3d.props.cameraFog > 1 : false,
-            clipFar: this.plugin.canvas3d?.props.cameraClipFar
+            occlusion: t.postprocessing.occlusionEnable,
+            outline: t.postprocessing.outlineEnable,
+            fog: ctx.canvas3d ? t.cameraFog > 1 : false,
+            clipFar: t.cameraClipFar
+        };
+    },
+    update(s, t) {
+        t.trackball.spin = s.spin;
+        t.cameraMode = s.camera;
+        t.transparentBackground = s.background.name === 'transparent';
+        t.renderer.backgroundColor = s.background.name === 'transparent' ? ColorNames.white : s.background.params.color;
+        switch (s.renderStyle) {
+            case 'flat': Object.assign(t.renderer, { lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }); break;
+            case 'matte':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }); break;
+            case 'glossy':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }); break;
+            case 'metallic':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }); break;
         }
+        t.postprocessing.occlusionEnable = s.occlusion;
+        if (s.occlusion) { 
+            t.postprocessing.occlusionBias = 0.5;
+            t.postprocessing.occlusionRadius = 64;
+        }
+        t.postprocessing.outlineEnable = s.outline;
+        t.cameraFog = s.fog ? 50 : 0;
+        t.cameraClipFar = s.clipFar;
+    },
+    apply(settings, ctx) {
+        return PluginCommands.Canvas3D.SetSettings.dispatch(ctx, { settings });
     }
-
-    componentDidMount() {
-        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
-    }
-
-    render() {
-        return <ParameterControls params={SimpleSettingsParams} values={this.values} onChange={this.setSettings} />
-    }
-}
+})

+ 37 - 0
src/mol-util/param-mapping.ts

@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ParamDefinition as PD } from './param-definition';
+import produce from 'immer'
+import { Mutable } from './type-helpers';
+
+export interface ParamMapping<S, T, Ctx> {
+    params(ctx: Ctx): PD.For<S>,
+    getTarget(ctx: Ctx): T,
+    getValues(t: T, ctx: Ctx): S,
+    update(s: S, ctx: Ctx): T,
+    apply(t: T, ctx: Ctx): void | Promise<void>
+}
+
+export function ParamMapping<S, T, Ctx>(def: {
+    params: ((ctx: Ctx) => PD.For<S>) | PD.For<S>,
+    target(ctx: Ctx): T
+}): (options: {
+    values(t: T, ctx: Ctx): S,
+    update(s: S, t: Mutable<T>, ctx: Ctx): void,
+    apply?(t: T, ctx: Ctx): void | Promise<void>
+}) => ParamMapping<S, T, Ctx> {
+    return ({ values, update, apply }) => ({
+        params: typeof def.params === 'function' ? def.params as any : ctx => def.params,
+        getTarget: def.target,
+        getValues: values,
+        update(s, ctx) {
+            const t = def.target(ctx);
+            return produce(t, t1 => update(s, t1 as any, ctx));
+        },
+        apply: apply ? apply : () => { }
+    });
+}