Bladeren bron

wip, crystal cell assemblies, refactoring

Alexander Rose 5 jaren geleden
bovenliggende
commit
8f89746497
3 gewijzigde bestanden met toevoegingen van 458 en 343 verwijderingen
  1. 3 343
      src/structure-viewer/ui/controls.tsx
  2. 117 0
      src/structure-viewer/ui/general.tsx
  3. 338 0
      src/structure-viewer/ui/structure.tsx

+ 3 - 343
src/structure-viewer/ui/controls.tsx

@@ -10,18 +10,11 @@ import { PluginContextContainer } from 'molstar/lib/mol-plugin/ui/plugin';
 import { TransformUpdaterControl } from 'molstar/lib/mol-plugin/ui/state/update-transform';
 import { StructureToolsWrapper } from 'molstar/lib/mol-plugin/ui/controls';
 import { StateElements } from '../helpers';
-import { ParameterControls } from 'molstar/lib/mol-plugin/ui/controls/parameters';
-import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
-import { PluginCommands } from 'molstar/lib/mol-plugin/command';
-import { Canvas3DParams } from 'molstar/lib/mol-canvas3d/canvas3d';
-import { StateObject, StateBuilder, StateTree } from 'molstar/lib/mol-state';
-import { PluginStateObject as PSO } from 'molstar/lib/mol-plugin/state/objects';
-import { StateTransforms } from 'molstar/lib/mol-plugin/state/transforms';
-import { StructureSelectionQueries as Q } from 'molstar/lib/mol-plugin/util/structure-selection-helper';
-import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
 import { Viewport, ViewportControls } from 'molstar/lib/mol-plugin/ui/viewport';
 import { BackgroundTaskProgress } from 'molstar/lib/mol-plugin/ui/task';
 import { LociLabelControl } from 'molstar/lib/mol-plugin/ui/controls';
+import { GeneralSettings } from './general';
+import { StructureControls } from './structure';
 
 export class ControlsWrapper extends PluginUIComponent {
     componentDidMount() {
@@ -33,11 +26,7 @@ export class ControlsWrapper extends PluginUIComponent {
         return <div className='msp-scrollable-container msp-right-controls'>
             <PluginContextContainer plugin={this.plugin}>
                 <GeneralSettings />
-                <StructureControls
-                    trajectoryRef={StateElements.Trajectory}
-                    modelRef={StateElements.Model}
-                    assemblyRef={StateElements.Assembly}
-                />
+                <StructureControls />
                 <TransformUpdaterControl nodeRef={StateElements.VolumeStreaming} header={{ name: 'Volume Controls', description: '' }} />
                 <StructureToolsWrapper />
             </PluginContextContainer>
@@ -56,333 +45,4 @@ export class ViewportWrapper extends PluginUIComponent {
             <LociLabelControl />
         </>;
     }
-}
-
-//
-
-const GeneralSettingsParams = {
-    spin: Canvas3DParams.trackball.params.spin,
-    backgroundColor: Canvas3DParams.renderer.params.backgroundColor,
-    renderStyle: PD.Select('glossy', [['toon', 'Toon'], ['matte', 'Matte'], ['glossy', 'Glossy'], ['metallic', 'Metallic']]),
-    occlusion: PD.Boolean(false),
-}
-
-type GeneralSettingsState = { isCollapsed?: boolean }
-
-class GeneralSettings<P, S extends GeneralSettingsState> extends PluginUIComponent<P, S> {
-    setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
-        if (p.name === 'spin') {
-            const trackball = this.plugin.canvas3d.props.trackball;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: p.value } } });
-        } else if (p.name === 'backgroundColor') {
-            const renderer = this.plugin.canvas3d.props.renderer;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: p.value } } });
-        } else if (p.name === 'renderStyle') {
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            const renderer = this.plugin.canvas3d.props.renderer;
-            if (p.value === 'toon') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    postprocessing: { ...postprocessing, outlineEnable: true, },
-                    renderer: { ...renderer, lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'matte') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    postprocessing: { ...postprocessing, outlineEnable: false, },
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }
-                } });
-            } else if (p.value === 'glossy') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    postprocessing: { ...postprocessing, outlineEnable: false, },
-                    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: {
-                    postprocessing: { ...postprocessing, outlineEnable: false, },
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }
-                } });
-            }
-        } else if (p.name === 'occlusion') {
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, occlusionEnable: p.value },
-            } });
-        }
-    }
-
-    get values () {
-        let renderStyle = 'custom'
-        const postprocessing = this.plugin.canvas3d.props.postprocessing;
-        const renderer = this.plugin.canvas3d.props.renderer;
-        if (postprocessing.outlineEnable) {
-            if (renderer.lightIntensity === 0 && renderer.ambientIntensity === 1 && renderer.roughness === 0.4 && renderer.metalness === 0) {
-                renderStyle = 'toon'
-            }
-        } else if (renderer.lightIntensity === 0.6 && renderer.ambientIntensity === 0.4) {
-            if (renderer.roughness === 1 && renderer.metalness === 0) {
-                renderStyle = 'matte'
-            } else if (renderer.roughness === 0.4 && renderer.metalness === 0) {
-                renderStyle = 'glossy'
-            } else if (renderer.roughness === 0.6 && renderer.metalness === 0.4) {
-                renderStyle = 'metallic'
-            }
-        }
-
-        return {
-            spin: this.plugin.canvas3d.props.trackball.spin,
-            backgroundColor: this.plugin.canvas3d.props.renderer.backgroundColor,
-            renderStyle,
-            occlusion: this.plugin.canvas3d.props.postprocessing.occlusionEnable
-        }
-    }
-
-    componentDidMount() {
-        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
-    }
-
-    toggleExpanded = () => {
-        this.setState({ isCollapsed: !this.state.isCollapsed });
-    }
-
-    state = {
-        isCollapsed: false
-    } as Readonly<S>
-
-    render() {
-        const wrapClass = this.state.isCollapsed
-            ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
-            : 'msp-transform-wrapper';
-
-        return this.plugin.canvas3d ? <div className={wrapClass}>
-            <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
-                    General Settings
-                </button>
-            </div>
-            {!this.state.isCollapsed &&
-                <ParameterControls params={GeneralSettingsParams} values={this.values} onChange={this.setSettings} />
-            }
-        </div> : null;
-    }
-}
-
-//
-
-type StructureControlsState = {
-    isCollapsed: boolean
-}
-type StructureControlsProps = {
-    trajectoryRef: string
-    modelRef: string
-    assemblyRef: string
-}
-
-class StructureControls<P extends StructureControlsProps, S extends StructureControlsState> extends PluginUIComponent<P, S> {
-    private applyState(tree: StateBuilder) {
-        return PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
-    }
-
-    async preset() {
-        const { structureRepresentation: rep } = this.plugin.helpers
-        await rep.setFromExpression('add', 'cartoon', Q.all)
-        await rep.setFromExpression('add', 'carbohydrate', Q.all)
-        await rep.setFromExpression('add', 'ball-and-stick', MS.struct.modifier.union([
-            MS.struct.combinator.merge([ Q.ligandsPlusConnected, Q.branchedConnectedOnly, Q.water ])
-        ]))
-    }
-
-    onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => {
-        console.log('onChange', p.name, p.value)
-        const { themeCtx } = this.plugin.structureRepresentation
-        const state = this.plugin.state.dataState;
-        const tree = state.build();
-        if (p.name === 'assembly') {
-            tree.to(StateElements.Assembly).update(
-                StateTransforms.Model.StructureAssemblyFromModel,
-                props => ({ ...props, id: p.value })
-            );
-            await this.applyState(tree)
-            await this.preset()
-        } else if (p.name === 'model') {
-            if (p.value === -1) {
-                tree.delete(StateElements.Model)
-                    .to(StateElements.Trajectory).apply(
-                        StateTransforms.Model.StructureFromTrajectory,
-                        {}, { ref: StateElements.Assembly }
-                    )
-                await this.applyState(tree);
-                await this.preset()
-            } else {
-                if (state.tree.transforms.has(StateElements.Model)) {
-                    tree.to(StateElements.Model).update(
-                        StateTransforms.Model.ModelFromTrajectory,
-                        props => ({ ...props, modelIndex: p.value })
-                    );
-                    await this.applyState(tree);
-                } else {
-                    tree.delete(StateElements.Assembly)
-                        .to(StateElements.Trajectory).apply(
-                            StateTransforms.Model.ModelFromTrajectory,
-                            { modelIndex: p.value }, { ref: StateElements.Model }
-                        )
-                        .apply(
-                            StateTransforms.Model.StructureAssemblyFromModel,
-                            { id: 'deposited' }, { ref: StateElements.Assembly }
-                        );
-                    await this.applyState(tree);
-                    await this.preset()
-                }
-            }
-        } else if (p.name === 'colorThemes') {
-            const assembly = this.getAssembly()
-            const dataCtx = { structure: assembly && assembly.data }
-
-            Object.keys(p.value).forEach(k => {
-                const repr = this.getRepresentation(k)
-                if (repr && repr.params) {
-                    const values = PD.getDefaultValues(themeCtx.colorThemeRegistry.get(name).getParams(dataCtx))
-                    tree.to(repr.transform.ref).update(
-                        StateTransforms.Representation.StructureRepresentation3D,
-                        props => ({ ...props, colorTheme: { name: p.value[k], params: values }})
-                    )
-                }
-            })
-            await this.applyState(tree)
-        }
-    }
-
-    getRepresentation(type: string) {
-        return this.plugin.helpers.structureRepresentation.getRepresentation(StateElements.Assembly, type)
-    }
-
-    getParams = () => {
-        const { themeCtx, registry } = this.plugin.structureRepresentation
-        const trajectory = this.getTrajectory()
-        const model = this.getModel()
-        const assembly = this.getAssembly()
-
-        const modelOptions: [number, string][] = []
-        if (trajectory) {
-            if (trajectory.data.length > 1) modelOptions.push([-1, `All`])
-            for (let i = 0, il = trajectory.data.length; i < il; ++i) {
-                modelOptions.push([i, `${i + 1}`])
-            }
-        }
-
-        const assemblyOptions: [string, string][] = [['deposited', 'deposited']]
-        let modelValue = 0
-        if (model) {
-            if (trajectory) modelValue = trajectory.data.indexOf(model.data)
-            const { assemblies } = model.data.symmetry
-            for (let i = 0, il = assemblies.length; i < il; ++i) {
-                const a = assemblies[i]
-                assemblyOptions.push([a.id, `${a.id}: ${a.details}`])
-            }
-        } else if (assembly) {
-            modelValue = -1
-        }
-
-        let assemblyValue = 'deposited'
-        let colorTypes = themeCtx.colorThemeRegistry.types
-        let types = registry.types
-        if (assembly) {
-            assemblyValue = assembly.data.units[0].conformation.operator.assembly.id
-            colorTypes = themeCtx.colorThemeRegistry.getApplicableTypes({ structure: assembly.data })
-            types = registry.getApplicableTypes(assembly.data)
-        }
-
-        const colorThemes: { [k: string]: PD.Any } = {}
-        for (let i = 0, il = types.length; i < il; ++i) {
-            const name = types[i][0]
-            if (this.getRepresentation(name)) {
-                colorThemes[name] = PD.Select(registry.get(name).defaultColorTheme, colorTypes)
-            }
-        }
-
-        return {
-            assembly: PD.Select(assemblyValue, assemblyOptions),
-            model: PD.Select(modelValue, modelOptions),
-            symmetry: PD.Select('todo', [['todo', 'todo']]),
-            colorThemes: PD.Group(colorThemes, { isExpanded: true }),
-        }
-    }
-
-    get values () {
-        const trajectory = this.getTrajectory()
-        const model = this.getModel()
-        const assembly = this.getAssembly()
-
-        const { registry } = this.plugin.structureRepresentation
-        const types = assembly ? registry.getApplicableTypes(assembly.data) : registry.types
-
-        const colorThemes: { [k: string]: string } = {}
-        for (let i = 0, il = types.length; i < il; ++i) {
-            const type = types[i][0]
-            const r = this.getRepresentation(type)
-            colorThemes[type] = r && r.params ? r.params.values.colorTheme.name : registry.get(type).defaultColorTheme
-        }
-
-        let modelValue = 0
-        if (trajectory) {
-            modelValue = model ? trajectory.data.indexOf(model.data) : -1
-        }
-
-        return {
-            assembly: assembly ? (assembly.data.units[0].conformation.operator.assembly.id || 'deposited') : 'deposited',
-            model: modelValue,
-            symmetry: 'todo',
-            colorThemes,
-        }
-    }
-
-    componentDidMount() {
-        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
-            if (StateTree.subtreeHasRef(state.tree, this.props.trajectoryRef, ref)) this.forceUpdate();
-        });
-
-        this.subscribe(this.plugin.events.state.object.created, ({ ref, state }) => {
-            if (StateTree.subtreeHasRef(state.tree, this.props.trajectoryRef, ref)) this.forceUpdate();
-        });
-    }
-
-    toggleExpanded = () => {
-        this.setState({ isCollapsed: !this.state.isCollapsed });
-    }
-
-    state = {
-        isCollapsed: false,
-    } as Readonly<S>
-
-    private getObj<T extends StateObject>(ref: string): T | undefined {
-        const state = this.plugin.state.dataState;
-        const cell = state.select(ref)[0];
-        if (!cell || !cell.obj) return void 0;
-        return (cell.obj as T);
-    }
-
-    private getTrajectory() { return this.getObj<PSO.Molecule.Trajectory>(this.props.trajectoryRef) }
-    private getModel() { return this.getObj<PSO.Molecule.Model>(this.props.modelRef) }
-    private getAssembly() { return this.getObj<PSO.Molecule.Structure>(this.props.assemblyRef) }
-
-    render() {
-        const trajectory = this.getTrajectory()
-        // const model = this.getModel()
-        const assembly = this.getAssembly()
-
-        if (!trajectory || !assembly) return null;
-
-        const wrapClass = this.state.isCollapsed
-            ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
-            : 'msp-transform-wrapper';
-
-        return this.plugin.canvas3d ? <div className={wrapClass}>
-            <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
-                    Structure Settings
-                </button>
-            </div>
-            {!this.state.isCollapsed &&
-                <ParameterControls params={this.getParams()} values={this.values} onChange={this.onChange} />
-            }
-        </div> : null;
-    }
 }

+ 117 - 0
src/structure-viewer/ui/general.tsx

@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from 'molstar/lib/mol-plugin/ui/base';
+import { ParameterControls } from 'molstar/lib/mol-plugin/ui/controls/parameters';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { PluginCommands } from 'molstar/lib/mol-plugin/command';
+import { Canvas3DParams } from 'molstar/lib/mol-canvas3d/canvas3d';
+
+const GeneralSettingsParams = {
+    spin: Canvas3DParams.trackball.params.spin,
+    backgroundColor: Canvas3DParams.renderer.params.backgroundColor,
+    renderStyle: PD.Select('glossy', [['toon', 'Toon'], ['matte', 'Matte'], ['glossy', 'Glossy'], ['metallic', 'Metallic']]),
+    occlusion: PD.Boolean(false),
+}
+
+type GeneralSettingsState = { isCollapsed?: boolean }
+
+export class GeneralSettings<P, S extends GeneralSettingsState> extends PluginUIComponent<P, S> {
+    setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        if (p.name === 'spin') {
+            const trackball = this.plugin.canvas3d.props.trackball;
+            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: p.value } } });
+        } else if (p.name === 'backgroundColor') {
+            const renderer = this.plugin.canvas3d.props.renderer;
+            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: p.value } } });
+        } else if (p.name === 'renderStyle') {
+            const postprocessing = this.plugin.canvas3d.props.postprocessing;
+            const renderer = this.plugin.canvas3d.props.renderer;
+            if (p.value === 'toon') {
+                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
+                    postprocessing: { ...postprocessing, outlineEnable: true, },
+                    renderer: { ...renderer, lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }
+                } });
+            } else if (p.value === 'matte') {
+                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
+                    postprocessing: { ...postprocessing, outlineEnable: false, },
+                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }
+                } });
+            } else if (p.value === 'glossy') {
+                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
+                    postprocessing: { ...postprocessing, outlineEnable: false, },
+                    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: {
+                    postprocessing: { ...postprocessing, outlineEnable: false, },
+                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }
+                } });
+            }
+        } else if (p.name === 'occlusion') {
+            const postprocessing = this.plugin.canvas3d.props.postprocessing;
+            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
+                postprocessing: { ...postprocessing, occlusionEnable: p.value, occlusionBias: 0.9 },
+            } });
+        }
+    }
+
+    get values () {
+        let renderStyle = 'custom'
+        const postprocessing = this.plugin.canvas3d.props.postprocessing;
+        const renderer = this.plugin.canvas3d.props.renderer;
+        if (postprocessing.outlineEnable) {
+            if (renderer.lightIntensity === 0 && renderer.ambientIntensity === 1 && renderer.roughness === 0.4 && renderer.metalness === 0) {
+                renderStyle = 'toon'
+            }
+        } else if (renderer.lightIntensity === 0.6 && renderer.ambientIntensity === 0.4) {
+            if (renderer.roughness === 1 && renderer.metalness === 0) {
+                renderStyle = 'matte'
+            } else if (renderer.roughness === 0.4 && renderer.metalness === 0) {
+                renderStyle = 'glossy'
+            } else if (renderer.roughness === 0.6 && renderer.metalness === 0.4) {
+                renderStyle = 'metallic'
+            }
+        }
+
+        return {
+            spin: this.plugin.canvas3d.props.trackball.spin,
+            backgroundColor: this.plugin.canvas3d.props.renderer.backgroundColor,
+            renderStyle,
+            occlusion: this.plugin.canvas3d.props.postprocessing.occlusionEnable
+        }
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    toggleExpanded = () => {
+        this.setState({ isCollapsed: !this.state.isCollapsed });
+    }
+
+    state = {
+        isCollapsed: false
+    } as Readonly<S>
+
+    render() {
+        const wrapClass = this.state.isCollapsed
+            ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
+            : 'msp-transform-wrapper';
+
+        return this.plugin.canvas3d ? <div className={wrapClass}>
+            <div className='msp-transform-header'>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    General Settings
+                </button>
+            </div>
+            {!this.state.isCollapsed &&
+                <ParameterControls params={GeneralSettingsParams} values={this.values} onChange={this.setSettings} />
+            }
+        </div> : null;
+    }
+}

+ 338 - 0
src/structure-viewer/ui/structure.tsx

@@ -0,0 +1,338 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from 'molstar/lib/mol-plugin/ui/base';
+import { StateElements } from '../helpers';
+import { ParameterControls } from 'molstar/lib/mol-plugin/ui/controls/parameters';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { PluginCommands } from 'molstar/lib/mol-plugin/command';
+import { StateObject, StateBuilder, StateTree, StateSelection } from 'molstar/lib/mol-state';
+import { PluginStateObject as PSO } from 'molstar/lib/mol-plugin/state/objects';
+import { StateTransforms } from 'molstar/lib/mol-plugin/state/transforms';
+import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
+import { Model } from 'molstar/lib/mol-model/structure';
+
+type StructureControlsState = {
+    isCollapsed: boolean
+    trajectoryRef: string
+}
+type StructureControlsProps = {
+
+}
+
+export class StructureControls<P extends StructureControlsProps, S extends StructureControlsState> extends PluginUIComponent<P, S> {
+    private applyState(tree: StateBuilder) {
+        return PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
+    }
+
+    async preset() {
+        await this.plugin.helpers.structureRepresentation.preset()
+    }
+
+    async setAssembly(id: string) {
+        const state = this.plugin.state.dataState;
+        const tree = state.build();
+        if (id === '__unitcell__') {
+            const props = { ijkMin: Vec3.create(0, 0, 0), ijkMax: Vec3.create(0, 0, 0) }
+            tree.delete(StateElements.Assembly)
+                .to(StateElements.Model).apply(
+                    StateTransforms.Model.StructureSymmetryFromModel,
+                    props, { ref: StateElements.Assembly, tags: [ 'unitcell' ] }
+                )
+        } else if (id === '__supercell__') {
+            const props = { ijkMin: Vec3.create(-1, -1, -1), ijkMax: Vec3.create(1, 1, 1) }
+            tree.delete(StateElements.Assembly)
+                .to(StateElements.Model).apply(
+                    StateTransforms.Model.StructureSymmetryFromModel,
+                    props, { ref: StateElements.Assembly, tags: [ 'supercell' ] }
+                )
+        } else {
+            tree.delete(StateElements.Assembly)
+                .to(StateElements.Model).apply(
+                    StateTransforms.Model.StructureAssemblyFromModel,
+                    { id }, { ref: StateElements.Assembly }
+                )
+            // tree.to(StateElements.Assembly).update(
+            //     StateTransforms.Model.StructureAssemblyFromModel,
+            //     props => ({ ...props, id: p.value })
+            // );
+        }
+        await this.applyState(tree)
+        await this.preset()
+    }
+
+    async setModel(modelIndex: number) {
+        const state = this.plugin.state.dataState;
+        const tree = state.build();
+        if (modelIndex === -1) {
+            tree.delete(StateElements.Model)
+                .to(StateElements.Trajectory).apply(
+                    StateTransforms.Model.StructureFromTrajectory,
+                    {}, { ref: StateElements.Assembly }
+                )
+            await this.applyState(tree);
+            await this.preset()
+        } else {
+            if (state.tree.transforms.has(StateElements.Model)) {
+                tree.to(StateElements.Model).update(
+                    StateTransforms.Model.ModelFromTrajectory,
+                    props => ({ ...props, modelIndex })
+                );
+                await this.applyState(tree);
+            } else {
+                tree.delete(StateElements.Assembly)
+                    .to(StateElements.Trajectory).apply(
+                        StateTransforms.Model.ModelFromTrajectory,
+                        { modelIndex }, { ref: StateElements.Model }
+                    )
+                    .apply(
+                        StateTransforms.Model.StructureAssemblyFromModel,
+                        { id: 'deposited' }, { ref: StateElements.Assembly }
+                    );
+                await this.applyState(tree);
+                await this.preset()
+            }
+        }
+    }
+
+    async setColorTheme(theme: { [k: string]: string }) {
+        const { themeCtx } = this.plugin.structureRepresentation
+        const state = this.plugin.state.dataState;
+        const tree = state.build();
+
+        const assembly = this.getAssembly()
+        const dataCtx = { structure: assembly && assembly.data }
+
+        Object.keys(theme).forEach(k => {
+            const repr = this.getRepresentation(k)
+            if (repr && repr.params) {
+                const values = PD.getDefaultValues(themeCtx.colorThemeRegistry.get(name).getParams(dataCtx))
+                tree.to(repr.transform.ref).update(
+                    StateTransforms.Representation.StructureRepresentation3D,
+                    props => ({ ...props, colorTheme: { name: theme[k], params: values }})
+                )
+            }
+        })
+        await this.applyState(tree)
+    }
+
+    onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => {
+        console.log('onChange', p.name, p.value)
+        if (p.name === 'assembly') {
+            this.setAssembly(p.value)
+        } else if (p.name === 'model') {
+            this.setModel(p.value)
+        } else if (p.name === 'colorThemes') {
+            this.setColorTheme(p.value)
+        }
+    }
+
+    getRepresentation(type: string) {
+        return this.plugin.helpers.structureRepresentation.getRepresentation(StateElements.Assembly, type)
+    }
+
+    getParams = () => {
+        const { themeCtx, registry } = this.plugin.structureRepresentation
+        const trajectory = this.getTrajectory()
+        const model = this.getModel()
+        const assembly = this.getAssembly()
+
+        const modelOptions: [number, string][] = []
+        const assemblyOptions: [string, string][] = [['deposited', 'deposited']]
+
+        if (trajectory) {
+            if (trajectory.data.length > 1) modelOptions.push([-1, `All`])
+            for (let i = 0, il = trajectory.data.length; i < il; ++i) {
+                modelOptions.push([i, `${i + 1}`])
+            }
+            if (trajectory.data.length === 1 && modelHasSymmetry(trajectory.data[0])) {
+                assemblyOptions.push(['__unitcell__', 'unitcell'], ['__supercell__', 'supercell'])
+            }
+        }
+
+        let modelValue = 0
+        if (model) {
+            if (trajectory) modelValue = trajectory.data.indexOf(model.data)
+            const { assemblies } = model.data.symmetry
+            for (let i = 0, il = assemblies.length; i < il; ++i) {
+                const a = assemblies[i]
+                assemblyOptions.push([a.id, `${a.id}: ${a.details}`])
+            }
+        } else if (assembly) {
+            // assembly from trajectory, no model
+            modelValue = -1
+        }
+
+        let assemblyValue = 'deposited'
+        let colorTypes = themeCtx.colorThemeRegistry.types
+        let types = registry.types
+        if (assembly) {
+            assemblyValue = assembly.data.units[0].conformation.operator.assembly.id
+            colorTypes = themeCtx.colorThemeRegistry.getApplicableTypes({ structure: assembly.data })
+            types = registry.getApplicableTypes(assembly.data)
+        }
+
+        const colorThemes: { [k: string]: PD.Any } = {}
+        for (let i = 0, il = types.length; i < il; ++i) {
+            const name = types[i][0]
+            if (this.getRepresentation(name)) {
+                colorThemes[name] = PD.Select(registry.get(name).defaultColorTheme, colorTypes)
+            }
+        }
+
+        return {
+            assembly: PD.Select(assemblyValue, assemblyOptions),
+            model: PD.Select(modelValue, modelOptions),
+            symmetry: PD.Select('todo', [['todo', 'todo']]),
+            colorThemes: PD.Group(colorThemes, { isExpanded: true }),
+        }
+    }
+
+    get values () {
+        const trajectory = this.getTrajectory()
+        const model = this.getModel()
+        const assembly = this.getAssembly()
+
+        const { registry } = this.plugin.structureRepresentation
+        const types = assembly ? registry.getApplicableTypes(assembly.data) : registry.types
+
+        const colorThemes: { [k: string]: string } = {}
+        for (let i = 0, il = types.length; i < il; ++i) {
+            const type = types[i][0]
+            const r = this.getRepresentation(type)
+            colorThemes[type] = r && r.params ? r.params.values.colorTheme.name : registry.get(type).defaultColorTheme
+        }
+
+        let modelValue = 0
+        if (trajectory) {
+            modelValue = model ? trajectory.data.indexOf(model.data) : -1
+        }
+
+        let assemblyValue = 'deposited'
+        if (assembly) {
+            const tags = (assembly as StateObject).tags
+            if (tags && tags.includes('unitcell')) {
+                assemblyValue = '__unitcell__'
+            } else if (tags && tags.includes('supercell')) {
+                assemblyValue = '__supercell__'
+            } else {
+                assemblyValue = assembly.data.units[0].conformation.operator.assembly.id || 'deposited'
+            }
+        }
+
+        return {
+            assembly: assemblyValue,
+            model: modelValue,
+            symmetry: 'todo',
+            colorThemes,
+        }
+    }
+
+    private findTrajectoryRef() {
+        const trajectories = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Trajectory))
+        return trajectories.length > 0 ? trajectories[0].transform.ref : ''
+    }
+
+    componentDidMount() {
+        this.setState({ trajectoryRef: this.findTrajectoryRef() })
+
+        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+            if (!this.getTrajectory()) {
+                this.setState({ trajectoryRef: this.findTrajectoryRef() })
+            } else if (StateTree.subtreeHasRef(state.tree, this.state.trajectoryRef, ref)) {
+                this.forceUpdate()
+            }
+        })
+
+        this.subscribe(this.plugin.events.state.object.created, ({ ref, state }) => {
+            if (!this.getTrajectory()) {
+                this.setState({ trajectoryRef: this.findTrajectoryRef() })
+            } else if (StateTree.subtreeHasRef(state.tree, this.state.trajectoryRef, ref)) {
+                this.forceUpdate()
+            }
+        })
+
+        this.subscribe(this.plugin.events.state.object.removed, ({ ref, state }) => {
+            if (!this.getTrajectory()) {
+                this.setState({ trajectoryRef: this.findTrajectoryRef() })
+            } else if (StateTree.subtreeHasRef(state.tree, this.state.trajectoryRef, ref)) {
+                this.forceUpdate()
+            }
+        })
+    }
+
+    toggleExpanded = () => {
+        this.setState({ isCollapsed: !this.state.isCollapsed })
+    }
+
+    state = {
+        isCollapsed: false,
+        trajectoryRef: ''
+    } as S
+
+    private getObj<T extends StateObject>(ref: string): T | undefined {
+        if (!ref) return undefined
+        const state = this.plugin.state.dataState
+        const cell = state.select(ref)[0]
+        if (!cell || !cell.obj) return undefined
+        return (cell.obj as T)
+    }
+
+    private getTrajectory() {
+        return this.getObj<PSO.Molecule.Trajectory>(this.state.trajectoryRef)
+    }
+
+    private getModel() {
+        if (!this.state.trajectoryRef) return
+        const models = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Model, this.state.trajectoryRef))
+        return models.length > 0 ? models[0].obj : undefined
+    }
+
+    private getAssembly() {
+        if (!this.state.trajectoryRef) return
+        const assemblies = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure, this.state.trajectoryRef))
+        return assemblies.length > 0 ? assemblies[0].obj : undefined
+    }
+
+    render() {
+        const trajectory = this.getTrajectory()
+        // const model = this.getModel()
+        const assembly = this.getAssembly()
+
+        if (!trajectory || !assembly) return null;
+
+        const wrapClass = this.state.isCollapsed
+            ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
+            : 'msp-transform-wrapper';
+
+        return this.plugin.canvas3d ? <div className={wrapClass}>
+            <div className='msp-transform-header'>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    Structure Settings
+                </button>
+            </div>
+            {!this.state.isCollapsed &&
+                <ParameterControls params={this.getParams()} values={this.values} onChange={this.onChange} />
+            }
+        </div> : null;
+    }
+}
+
+function modelHasSymmetry(model: Model) {
+    const mmcif = model.sourceData.data
+    return (
+        mmcif.symmetry._rowCount === 1 && mmcif.cell._rowCount === 1 && !(
+            mmcif.symmetry.Int_Tables_number.value(0) === 1 &&
+            mmcif.cell.angle_alpha.value(0) === 90 &&
+            mmcif.cell.angle_beta.value(0) === 90 &&
+            mmcif.cell.angle_gamma.value(0) === 90 &&
+            mmcif.cell.length_a.value(0) === 1 &&
+            mmcif.cell.length_b.value(0) === 1 &&
+            mmcif.cell.length_c.value(0) === 1
+        )
+    )
+}