Browse Source

refactored assembly-symmetry

- provide a single symmetry-index, story data in AssemblySymmetryDataProvider
Alexander Rose 5 years ago
parent
commit
50d8debb2b

+ 50 - 13
src/mol-model-props/rcsb/assembly-symmetry.ts

@@ -50,7 +50,7 @@ export namespace AssemblySymmetry {
         return BiologicalAssemblyNames.has(details)
     }
 
-    export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryProps): Promise<AssemblySymmetryValue> {
+    export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<AssemblySymmetryDataValue> {
         if (!isApplicable(structure)) return []
 
         const client = new GraphQLClient(props.serverUrl, ctx.fetch)
@@ -64,23 +64,23 @@ export namespace AssemblySymmetry {
             console.error('expected `rcsb_struct_symmetry` field')
             return []
         }
-        return result.assembly.rcsb_struct_symmetry as AssemblySymmetryValue
+        return result.assembly.rcsb_struct_symmetry as AssemblySymmetryDataValue
     }
 
     export type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }>
-    export function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes {
+    export function isRotationAxes(x: AssemblySymmetryValue['rotation_axes']): x is RotationAxes {
         return !!x && x.length > 0
     }
 }
 
 export function getSymmetrySelectParam(structure?: Structure) {
-    const param = PD.Select<number>(-1, [[-1, 'No Symmetries']])
+    const param = PD.Select<number>(0, [[0, 'First Symmetry']])
     if (structure) {
-        const assemblySymmetry = AssemblySymmetryProvider.get(structure).value
-        if (assemblySymmetry) {
+        const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value
+        if (assemblySymmetryData) {
             const options: [number, string][] = []
-            for (let i = 0, il = assemblySymmetry.length; i < il; ++i) {
-                const { symbol, kind } = assemblySymmetry[i]
+            for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
+                const { symbol, kind } = assemblySymmetryData[i]
                 if (symbol !== 'C1') {
                     options.push([ i, `${i + 1}: ${symbol} ${kind}` ])
                 }
@@ -94,13 +94,46 @@ export function getSymmetrySelectParam(structure?: Structure) {
     return param
 }
 
-export const AssemblySymmetryParams = {
+//
+
+export const AssemblySymmetryDataParams = {
     serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL' })
 }
+export type AssemblySymmetryDataParams = typeof AssemblySymmetryDataParams
+export type AssemblySymmetryDataProps = PD.Values<AssemblySymmetryDataParams>
+
+export type AssemblySymmetryDataValue = NonNullableArray<NonNullable<NonNullable<AssemblySymmetryQuery['assembly']>['rcsb_struct_symmetry']>>
+
+export const AssemblySymmetryDataProvider: CustomStructureProperty.Provider<AssemblySymmetryDataParams, AssemblySymmetryDataValue> = CustomStructureProperty.createProvider({
+    label: 'Assembly Symmetry Data',
+    descriptor: CustomPropertyDescriptor({
+        name: 'rcsb_struct_symmetry_data',
+        // TODO `cifExport` and `symbol`
+    }),
+    type: 'root',
+    defaultParams: AssemblySymmetryDataParams,
+    getParams: (data: Structure) => AssemblySymmetryDataParams,
+    isApplicable: (data: Structure) => AssemblySymmetry.isApplicable(data),
+    obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<AssemblySymmetryDataProps>) => {
+        const p = { ...PD.getDefaultValues(AssemblySymmetryDataParams), ...props }
+        return await AssemblySymmetry.fetch(ctx, data, p)
+    }
+})
+
+//
+
+function getAssemblySymmetryParams(data?: Structure) {
+    return {
+        ... AssemblySymmetryDataParams,
+        symmetryIndex: getSymmetrySelectParam(data)
+    }
+}
+
+export const AssemblySymmetryParams = getAssemblySymmetryParams()
 export type AssemblySymmetryParams = typeof AssemblySymmetryParams
 export type AssemblySymmetryProps = PD.Values<AssemblySymmetryParams>
 
-export type AssemblySymmetryValue = NonNullableArray<NonNullable<NonNullable<AssemblySymmetryQuery['assembly']>['rcsb_struct_symmetry']>>
+export type AssemblySymmetryValue = AssemblySymmetryDataValue[0]
 
 export const AssemblySymmetryProvider: CustomStructureProperty.Provider<AssemblySymmetryParams, AssemblySymmetryValue> = CustomStructureProperty.createProvider({
     label: 'Assembly Symmetry',
@@ -110,10 +143,14 @@ export const AssemblySymmetryProvider: CustomStructureProperty.Provider<Assembly
     }),
     type: 'root',
     defaultParams: AssemblySymmetryParams,
-    getParams: (data: Structure) => AssemblySymmetryParams,
+    getParams: getAssemblySymmetryParams,
     isApplicable: (data: Structure) => AssemblySymmetry.isApplicable(data),
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<AssemblySymmetryProps>) => {
-        const p = { ...PD.getDefaultValues(AssemblySymmetryParams), ...props }
-        return await AssemblySymmetry.fetch(ctx, data, p)
+        const p = { ...PD.getDefaultValues(getAssemblySymmetryParams(data)), ...props }
+        await AssemblySymmetryDataProvider.attach(ctx, data, p)
+        const assemblySymmetryData = AssemblySymmetryDataProvider.get(data).value
+        const assemblySymmetry = assemblySymmetryData?.[p.symmetryIndex]
+        if (!assemblySymmetry) new Error(`No assembly symmetry found for index ${p.symmetryIndex}`)
+        return assemblySymmetry
     }
 })

+ 8 - 9
src/mol-model-props/rcsb/representations/assembly-symmetry.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry';
+import { AssemblySymmetryValue, AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
@@ -51,7 +51,6 @@ function axesColorHelp(value: { name: string, params: {} }) {
 const SharedParams = {
     ...Mesh.Params,
     scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
-    symmetryIndex: getSymmetrySelectParam(),
 }
 
 const AxesParams = {
@@ -113,9 +112,9 @@ const getOrderPrimitive = memoize1((order: number): Primitive | undefined => {
 })
 
 function getAxesMesh(data: AssemblySymmetryValue, props: PD.Values<AxesParams>, mesh?: Mesh) {
-    const { symmetryIndex, scale } = props
+    const { scale } = props
 
-    const { rotation_axes } = data[symmetryIndex]
+    const { rotation_axes } = data
     if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
 
     const { start, end } = rotation_axes[0]
@@ -158,7 +157,7 @@ function getAxesShape(ctx: RuntimeContext, data: Structure, props: AssemblySymme
     const geo = getAxesMesh(assemblySymmetry, props, shape && shape.geometry);
     const getColor = (groupId: number) => {
         if (props.axesColor.name === 'byOrder') {
-            const { rotation_axes } = assemblySymmetry[props.symmetryIndex]
+            const { rotation_axes } = assemblySymmetry
             const order = rotation_axes![groupId]?.order
             if (order === 2) return OrderColors[2]
             else if (order === 3) return OrderColors[3]
@@ -168,7 +167,7 @@ function getAxesShape(ctx: RuntimeContext, data: Structure, props: AssemblySymme
         }
     }
     const getLabel = (groupId: number) => {
-        const { type, symbol, kind, rotation_axes } = assemblySymmetry[props.symmetryIndex]
+        const { type, symbol, kind, rotation_axes } = assemblySymmetry
         const order = rotation_axes![groupId]?.order
         return [
             `<small>${data.model.entryId}</small>`,
@@ -279,9 +278,9 @@ function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.Rota
 
 function getCageMesh(data: Structure, props: PD.Values<CageParams>, mesh?: Mesh) {
     const assemblySymmetry = AssemblySymmetryProvider.get(data).value!
-    const { symmetryIndex, scale } = props
+    const { scale } = props
 
-    const { rotation_axes, symbol } = assemblySymmetry[symmetryIndex]
+    const { rotation_axes, symbol } = assemblySymmetry
     if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
 
     const cage = getSymbolCage(symbol)
@@ -308,7 +307,7 @@ function getCageShape(ctx: RuntimeContext, data: Structure, props: AssemblySymme
         return props.cageColor
     }
     const getLabel = (groupId: number) => {
-        const { type, symbol, kind } = assemblySymmetry[props.symmetryIndex]
+        const { type, symbol, kind } = assemblySymmetry
         data.model.entryId
         return [
             `<small>${data.model.entryId}</small>`,

+ 2 - 5
src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts

@@ -7,7 +7,7 @@
 import { ThemeDataContext } from '../../../mol-theme/theme';
 import { ColorTheme, LocationColor } from '../../../mol-theme/color';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition'
-import { AssemblySymmetryProvider, AssemblySymmetry, getSymmetrySelectParam } from '../assembly-symmetry';
+import { AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry';
 import { Color } from '../../../mol-util/color';
 import { Unit, StructureElement, StructureProperties } from '../../../mol-model/structure';
 import { Location } from '../../../mol-model/location';
@@ -33,12 +33,10 @@ function clusterMemberKey(asymId: string, operList: string[]) {
 
 export const AssemblySymmetryClusterColorThemeParams = {
     ...getPaletteParams({ scaleList: 'red-yellow-blue' }),
-    symmetryIndex: getSymmetrySelectParam(),
 }
 export type AssemblySymmetryClusterColorThemeParams = typeof AssemblySymmetryClusterColorThemeParams
 export function getAssemblySymmetryClusterColorThemeParams(ctx: ThemeDataContext) {
     const params = PD.clone(AssemblySymmetryClusterColorThemeParams)
-    params.symmetryIndex = getSymmetrySelectParam(ctx.structure)
     return params
 }
 
@@ -46,11 +44,10 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
     let color: LocationColor = () => DefaultColor
     let legend: ScaleLegend | TableLegend | undefined
 
-    const { symmetryIndex } = props
     const assemblySymmetry = ctx.structure && AssemblySymmetryProvider.get(ctx.structure)
     const contextHash = assemblySymmetry?.version
 
-    const clusters = assemblySymmetry?.value?.[symmetryIndex]?.clusters
+    const clusters = assemblySymmetry?.value?.clusters
 
     if (clusters?.length && ctx.structure) {
         const clusterByMember = new Map<string, number>()

+ 14 - 11
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../../../mol-util/param-definition'
-import { AssemblySymmetryProvider, AssemblySymmetry, getSymmetrySelectParam } from '../../../../../mol-model-props/rcsb/assembly-symmetry';
+import { AssemblySymmetryProvider, AssemblySymmetry } from '../../../../../mol-model-props/rcsb/assembly-symmetry';
 import { PluginBehavior } from '../../../behavior';
 import { AssemblySymmetryParams, AssemblySymmetryRepresentation } from '../../../../../mol-model-props/rcsb/representations/assembly-symmetry';
 import { AssemblySymmetryClusterColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/assembly-symmetry-cluster';
@@ -17,6 +17,7 @@ import { GenericRepresentationRef } from '../../../../../mol-plugin-state/manage
 import { TrajectoryHierarchyPresetProvider } from '../../../../../mol-plugin-state/builder/structure/hierarchy-preset';
 import { RootStructureDefinition } from '../../../../../mol-plugin-state/helpers/root-structure';
 import { StateTransforms } from '../../../../../mol-plugin-state/transforms';
+import { AssemblySymmetryControls } from './ui/assembly-symmetry';
 
 const Tag = AssemblySymmetry.Tag
 
@@ -35,7 +36,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
             this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
             this.ctx.representation.structure.themes.colorThemeRegistry.add(AssemblySymmetryClusterColorThemeProvider)
 
-            this.ctx.customSourceControls.set(Tag.Representation, selection => {
+            this.ctx.genericRepresentationControls.set(Tag.Representation, selection => {
                 const refs: GenericRepresentationRef[] = []
                 selection.structures.forEach(structure => {
                     const symmRepr = structure.genericRepresentations?.filter(r => r.cell.transform.transformer.id === AssemblySymmetry3D.id)[0]
@@ -43,6 +44,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
                 })
                 return [refs, 'Symmetries']
             })
+            this.ctx.customStructureControls.set(Tag.Representation, AssemblySymmetryControls as any)
             this.ctx.builders.structure.hierarchy.registerPreset(assemblySymmetryPreset)
         }
 
@@ -58,7 +60,8 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
             this.ctx.representation.structure.themes.colorThemeRegistry.remove(AssemblySymmetryClusterColorThemeProvider)
 
-            this.ctx.customSourceControls.delete(Tag.Representation)
+            this.ctx.genericRepresentationControls.delete(Tag.Representation)
+            this.ctx.customStructureControls.delete(Tag.Representation)
             this.ctx.builders.structure.hierarchy.unregisterPreset(assemblySymmetryPreset)
         }
     },
@@ -70,7 +73,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
 
 //
 
-const InitAssemblySymmetry3D = StateAction.build({
+export const InitAssemblySymmetry3D = StateAction.build({
     display: {
         name: 'Assembly Symmetry',
         description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
@@ -102,7 +105,6 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
     params: (a) => {
         return {
             ...AssemblySymmetryParams,
-            symmetryIndex: getSymmetrySelectParam(a?.data),
         }
     }
 })({
@@ -113,12 +115,12 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
         return Task.create('Assembly Symmetry', async ctx => {
             await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value
-            if (!assemblySymmetry || assemblySymmetry.length === 0) {
+            if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
                 return StateObject.Null;
             }
             const repr = AssemblySymmetryRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => AssemblySymmetryParams)
             await repr.createOrUpdate(params, a.data).runInContext(ctx);
-            const { type, kind, symbol } = assemblySymmetry![params.symmetryIndex]
+            const { type, kind, symbol } = assemblySymmetry
             return new PluginStateObject.Shape.Representation3D({ repr, source: a }, { label: kind, description: `${type} (${symbol})` });
         });
     },
@@ -126,12 +128,12 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
         return Task.create('Assembly Symmetry', async ctx => {
             await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value
-            if (!assemblySymmetry || assemblySymmetry.length === 0) {
+            if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
                 return StateTransformer.UpdateResult.Recreate
             }
             const props = { ...b.data.repr.props, ...newParams }
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
-            const { type, kind, symbol } = assemblySymmetry![newParams.symmetryIndex]
+            const { type, kind, symbol } = assemblySymmetry
             b.label = kind
             b.description = `${type} (${symbol})`
             return StateTransformer.UpdateResult.Updated;
@@ -167,13 +169,14 @@ const assemblySymmetryPreset = TrajectoryHierarchyPresetProvider({
         const structure = await builder.createStructure(modelProperties || model, params.structure);
         const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties);
 
+        const unitcell = await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true });
         await tryCreateAssemblySymmetry(plugin, structureProperties)
-
         const representation =  await plugin.builders.structure.representation.applyPreset(structureProperties, params.representationPreset || 'auto', { globalThemeName: Tag.Cluster });
 
         return {
             model,
             modelProperties,
+            unitcell,
             structure,
             structureProperties,
             representation
@@ -184,7 +187,7 @@ const assemblySymmetryPreset = TrajectoryHierarchyPresetProvider({
 async function tryCreateAssemblySymmetry(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, params?: StateTransformer.Params<AssemblySymmetry3D>, initialState?: Partial<StateTransform.State>) {
     const state = plugin.state.data;
     const assemblySymmetry = state.build().to(structure)
-        .apply(AssemblySymmetry3D, { ...params, symmetryIndex: params?.symmetryIndex ?? 0 }, { state: initialState });
+        .apply(AssemblySymmetry3D, params, { state: initialState });
     await plugin.updateDataState(assemblySymmetry, { revertOnError: true });
     return assemblySymmetry.selector
 }

+ 125 - 0
src/mol-plugin/behavior/dynamic/custom-props/rcsb/ui/assembly-symmetry.tsx

@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import { CollapsableState, CollapsableControls } from '../../../../../../mol-plugin-ui/base';
+import { ApplyActionControl } from '../../../../../../mol-plugin-ui/state/apply-action';
+import { InitAssemblySymmetry3D, AssemblySymmetry3D } from '../assembly-symmetry';
+import { AssemblySymmetryProvider,  AssemblySymmetryProps, AssemblySymmetryDataProvider } from '../../../../../../mol-model-props/rcsb/assembly-symmetry';
+import { ParameterControls } from '../../../../../../mol-plugin-ui/controls/parameters';
+import { ParamDefinition as PD } from '../../../../../../mol-util/param-definition';
+import { StructureHierarchyManager } from '../../../../../../mol-plugin-state/manager/structure/hierarchy';
+
+interface AssemblySymmetryControlState extends CollapsableState {
+    isBusy: boolean
+    options: AssemblySymmetryProps
+}
+
+export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySymmetryControlState> {
+    protected defaultState(): AssemblySymmetryControlState {
+        return {
+            header: 'Assembly Symmetry',
+            isCollapsed: false,
+            isBusy: false,
+            isHidden: true,
+            options: PD.getDefaultValues(AssemblySymmetryProvider.defaultParams)
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => this.setState({ isHidden: !this.canEnable() }));
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => this.setState({ isBusy: v }));
+
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => this.setState({
+            description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
+        }));
+    }
+
+    get pivot() {
+        return this.plugin.managers.structure.hierarchy.selection.structures[0];
+    }
+
+    canEnable() {
+        const { selection } = this.plugin.managers.structure.hierarchy;
+        if (selection.structures.length !== 1) return false;
+        const pivot = this.pivot.cell;
+        if (!pivot.obj) return false;
+        return !!InitAssemblySymmetry3D.definition.isApplicable?.(pivot.obj, pivot.transform, this.plugin);
+    }
+
+    renderEnable() {
+        const pivot = this.pivot;
+        return <ApplyActionControl state={pivot.cell.parent} action={InitAssemblySymmetry3D} initiallyCollapsed={true} nodeRef={pivot.cell.transform.ref} simpleApply={{ header: 'Enable', icon: 'check' }} />;
+    }
+
+    renderNoSymmetries() {
+        return <div className='msp-control-row msp-row-text'>
+            <div>No Symmetries</div>
+        </div>;
+    }
+
+    get params() {
+        const structure = this.pivot.cell.obj?.data
+        const params = PD.clone(structure ? AssemblySymmetryProvider.getParams(structure) : AssemblySymmetryProvider.defaultParams)
+        params.serverUrl.isHidden = true
+        params.symmetryIndex.options = [[-1, 'Off'], ...params.symmetryIndex.options]
+        return params
+    }
+
+    async updateAssemblySymmetry(values: AssemblySymmetryProps) {
+        const s = this.pivot
+        const assemblySymmetryParams = AssemblySymmetryProvider.getParams(s.cell.obj?.data!);
+
+        if (s.properties) {
+            const oldParams = s.properties.cell.transform.params?.properties[AssemblySymmetryProvider.descriptor.name];
+            if (PD.areEqual(assemblySymmetryParams, oldParams, values)) return;
+
+            const b = this.plugin.state.data.build();
+            b.to(s.properties.cell).update(old => {
+                old.properties[AssemblySymmetryProvider.descriptor.name] = values;
+            });
+            await this.plugin.updateDataState(b);
+        } else {
+            const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
+            const params = PD.getDefaultValues(pd);
+            if (PD.areEqual(assemblySymmetryParams, params.properties[AssemblySymmetryProvider.descriptor.name], values)) return;
+            params.properties[AssemblySymmetryProvider.descriptor.name] = values;
+            await this.plugin.builders.structure.insertStructureProperties(s.cell, params);
+        }
+    }
+
+    paramsOnChange = (options: AssemblySymmetryProps) => {
+        this.setState({ options })
+        this.updateAssemblySymmetry(options)
+    }
+
+    get hasAssemblySymmetry3D() {
+        return !!this.pivot.genericRepresentations?.filter(r => r.cell.transform.transformer.id === AssemblySymmetry3D.id)[0]
+    }
+
+    get enable() {
+        return !this.hasAssemblySymmetry3D && this.state.options.symmetryIndex !== -1
+    }
+
+    get noSymmetries() {
+        const structure = this.pivot.cell.obj?.data
+        const data = structure && AssemblySymmetryDataProvider.get(structure).value
+        return data && data.filter(sym => sym.symbol !== 'C1').length === 0
+    }
+
+    renderParams() {
+        return <>
+            <ParameterControls params={this.params} values={this.state.options} onChangeValues={this.paramsOnChange} />
+        </>;
+    }
+
+    renderControls() {
+        if (!this.pivot) return null;
+        if (this.noSymmetries) return this.renderNoSymmetries();
+        if (this.enable) return this.renderEnable();
+        return this.renderParams();
+    }
+}