diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 9be0646bf8e61bb9525715eeb5d098d39f94a0ba..25bd3648cfee27be1f62c3cf5753f41f3c761411 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -30,7 +30,7 @@ import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version'; import { PluginLayout } from './layout'; import { List } from 'immutable'; import { StateTransformParameters } from './ui/state/common'; -import { DataFormatRegistry } from './state/actions/basic'; +import { DataFormatRegistry } from './state/actions/volume'; import { PluginBehavior } from './behavior/behavior'; export class PluginContext { diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 0a3a8d719e6d17fdc6595fb70bda19603f84db8e..b5bfe464ecfc60e2d494de36cf4b2d8cb51fc970 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -11,10 +11,10 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { PluginCommands } from './command'; import { PluginSpec } from './spec'; -import { DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenVolume, DownloadDensity } from './state/actions/basic'; import { StateTransforms } from './state/transforms'; import { PluginBehaviors } from './behavior'; import { AnimateModelIndex } from './state/animation/built-in'; +import { StateActions } from './state/actions'; function getParam(name: string, regex: string): string { let r = new RegExp(`${name}=(${regex})[&]?`, 'i'); @@ -23,11 +23,12 @@ function getParam(name: string, regex: string): string { export const DefaultPluginSpec: PluginSpec = { actions: [ - PluginSpec.Action(DownloadStructure), - PluginSpec.Action(DownloadDensity), - PluginSpec.Action(OpenStructure), - PluginSpec.Action(OpenVolume), - PluginSpec.Action(CreateComplexRepresentation), + PluginSpec.Action(StateActions.Structure.DownloadStructure), + PluginSpec.Action(StateActions.Volume.DownloadDensity), + PluginSpec.Action(StateActions.Structure.OpenStructure), + PluginSpec.Action(StateActions.Volume.OpenVolume), + PluginSpec.Action(StateActions.Structure.CreateComplexRepresentation), + PluginSpec.Action(StateActions.Structure.EnableModelCustomProps), PluginSpec.Action(StateTransforms.Data.Download), PluginSpec.Action(StateTransforms.Data.ParseCif), PluginSpec.Action(StateTransforms.Data.ParseCcp4), diff --git a/src/mol-plugin/providers/custom-prop.ts b/src/mol-plugin/providers/custom-prop.ts deleted file mode 100644 index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000 --- a/src/mol-plugin/providers/custom-prop.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/src/mol-plugin/providers/theme.ts b/src/mol-plugin/providers/theme.ts deleted file mode 100644 index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000 --- a/src/mol-plugin/providers/theme.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 84624ff7e3e050ed85c8ed67f911b260419f572c..801f23dafb710be39f183dc99d22963522803fb6 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -14,6 +14,15 @@ // border-bottom: 1px solid $entity-color-Group; // TODO separate color } +.msp-current-header { + height: $row-height; + line-height: $row-height; + margin-bottom: $control-spacing; + text-align: center; + font-weight: bold; + background: $default-background; +} + .msp-btn-row-group { display:flex; flex-direction:row; diff --git a/src/mol-plugin/state/actions.ts b/src/mol-plugin/state/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5b1fb6a04dfc810abadefeab19d3ef266a90826 --- /dev/null +++ b/src/mol-plugin/state/actions.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as Structure from './actions/structure' +import * as Volume from './actions/volume' + +export const StateActions = { + Structure, + Volume +} \ No newline at end of file diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts new file mode 100644 index 0000000000000000000000000000000000000000..3aadfee2de0702040851301e4d82530e857d85c8 --- /dev/null +++ b/src/mol-plugin/state/actions/structure.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginContext } from 'mol-plugin/context'; +import { StateAction, StateBuilder, StateSelection, StateTransformer } from 'mol-state'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { Download } from '../transforms/data'; +import { StructureRepresentation3DHelpers } from '../transforms/representation'; +import { CustomModelProperties } from '../transforms/model'; + +// TODO: "structure/volume parser provider" + +export { DownloadStructure }; +type DownloadStructure = typeof DownloadStructure +const DownloadStructure = StateAction.build({ + from: PluginStateObject.Root, + display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' }, + params: { + source: PD.MappedStatic('bcif-static', { + 'pdbe-updated': PD.Group({ + id: PD.Text('1cbs', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'rcsb': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'bcif-static': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'url': PD.Group({ + url: PD.Text(''), + format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]), + isBinary: PD.Boolean(false), + supportProps: PD.Boolean(false) + }, { isFlat: true }) + }, { + options: [ + ['pdbe-updated', 'PDBe Updated'], + ['rcsb', 'RCSB'], + ['bcif-static', 'BinaryCIF (static PDBe Updated)'], + ['url', 'URL'] + ] + }) + } +})(({ params, state }, ctx: PluginContext) => { + const b = state.build(); + const src = params.source; + let downloadParams: StateTransformer.Params<Download>; + + switch (src.name) { + case 'url': + downloadParams = { url: src.params.url, isBinary: src.params.isBinary }; + break; + case 'pdbe-updated': + downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` }; + break; + case 'rcsb': + downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` }; + break; + case 'bcif-static': + downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` }; + break; + default: throw new Error(`${(src as any).name} not supported.`); + } + + const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams); + const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif'); + return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps)); +}); + +export const OpenStructure = StateAction.build({ + display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' }, + from: PluginStateObject.Root, + params: { file: PD.File({ accept: '.cif,.bcif' }) } +})(({ params, state }, ctx: PluginContext) => { + const b = state.build(); + const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); + const traj = createModelTree(data, 'cif'); + return state.updateTree(createStructureTree(ctx, traj, false)); +}); + +function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') { + const parsed = format === 'cif' + ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) + : b.apply(StateTransforms.Model.TrajectoryFromPDB); + + return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); +} + +function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean) { + let root = b; + if (supportProps) { + root = root.apply(StateTransforms.Model.CustomModelProperties); + } + const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel); + complexRepresentation(ctx, structure); + + return root; +} + +function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon')); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 })); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); +} + +export const CreateComplexRepresentation = StateAction.build({ + display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' }, + from: PluginStateObject.Molecule.Structure +})(({ ref, state }, ctx: PluginContext) => { + const root = state.build().to(ref); + complexRepresentation(ctx, root); + return state.updateTree(root); +}); + +export const UpdateTrajectory = StateAction.build({ + display: { name: 'Update Trajectory' }, + params: { + action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), + by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) + } +})(({ params, state }) => { + const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model) + .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); + + const update = state.build(); + + if (params.action === 'reset') { + for (const m of models) { + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + () => ({ modelIndex: 0 })); + } + } else { + for (const m of models) { + const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); + if (!parent || !parent.obj) continue; + const traj = parent.obj as PluginStateObject.Molecule.Trajectory; + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + old => { + let modelIndex = (old.modelIndex + params.by!) % traj.data.length; + if (modelIndex < 0) modelIndex += traj.data.length; + return { modelIndex }; + }); + } + } + + return state.updateTree(update); +}); + +export const EnableModelCustomProps = StateAction.build({ + display: { name: 'Custom Properties', description: 'Enable the addition of custom properties to the model.' }, + from: PluginStateObject.Molecule.Model, + params(a, ctx: PluginContext) { + if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) }; + return { properties: ctx.customModelProperties.getSelect(a.data) }; + }, + isApplicable(a, t, ctx: PluginContext) { + return t.transformer !== CustomModelProperties; + } +})(({ ref, params, state }, ctx: PluginContext) => { + const root = state.build().to(ref).insert(CustomModelProperties, params); + return state.updateTree(root); +}); diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/volume.ts similarity index 64% rename from src/mol-plugin/state/actions/basic.ts rename to src/mol-plugin/state/actions/volume.ts index d1779e6614806d62aed9483b8c179fb4f1ec9b7d..14b041322786e9c3774c8fbd65450d9d359064cd 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/volume.ts @@ -5,169 +5,17 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ +import { VolumeIsoValue } from 'mol-model/volume'; import { PluginContext } from 'mol-plugin/context'; -import { StateTree, StateTransformer, StateObject, State, StateBuilder, StateSelection, StateAction } from 'mol-state'; +import { State, StateAction, StateBuilder, StateObject, StateTransformer } from 'mol-state'; +import { Task } from 'mol-task'; +import { ColorNames } from 'mol-util/color/tables'; +import { FileInfo, getFileInfo } from 'mol-util/file-info'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { PluginStateObject } from '../objects'; import { StateTransforms } from '../transforms'; import { Download } from '../transforms/data'; -import { StructureRepresentation3DHelpers, VolumeRepresentation3DHelpers } from '../transforms/representation'; -import { getFileInfo, FileInfo } from 'mol-util/file-info'; -import { Task } from 'mol-task'; -import { ColorNames } from 'mol-util/color/tables'; -import { VolumeIsoValue } from 'mol-model/volume'; - -// TODO: "structure/volume parser provider" - -export { DownloadStructure }; -type DownloadStructure = typeof DownloadStructure -const DownloadStructure = StateAction.build({ - from: PluginStateObject.Root, - display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' }, - params: { - source: PD.MappedStatic('bcif-static', { - 'pdbe-updated': PD.Group({ - id: PD.Text('1cbs', { label: 'Id' }), - supportProps: PD.Boolean(false) - }, { isFlat: true }), - 'rcsb': PD.Group({ - id: PD.Text('1tqn', { label: 'Id' }), - supportProps: PD.Boolean(false) - }, { isFlat: true }), - 'bcif-static': PD.Group({ - id: PD.Text('1tqn', { label: 'Id' }), - supportProps: PD.Boolean(false) - }, { isFlat: true }), - 'url': PD.Group({ - url: PD.Text(''), - format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]), - isBinary: PD.Boolean(false), - supportProps: PD.Boolean(false) - }, { isFlat: true }) - }, { - options: [ - ['pdbe-updated', 'PDBe Updated'], - ['rcsb', 'RCSB'], - ['bcif-static', 'BinaryCIF (static PDBe Updated)'], - ['url', 'URL'] - ] - }) - } -})(({ params, state }, ctx: PluginContext) => { - const b = state.build(); - const src = params.source; - let downloadParams: StateTransformer.Params<Download>; - - switch (src.name) { - case 'url': - downloadParams = { url: src.params.url, isBinary: src.params.isBinary }; - break; - case 'pdbe-updated': - downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` }; - break; - case 'rcsb': - downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` }; - break; - case 'bcif-static': - downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` }; - break; - default: throw new Error(`${(src as any).name} not supported.`); - } - - const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams); - const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif'); - return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps)); -}); - -export const OpenStructure = StateAction.build({ - display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' }, - from: PluginStateObject.Root, - params: { file: PD.File({ accept: '.cif,.bcif' }) } -})(({ params, state }, ctx: PluginContext) => { - const b = state.build(); - const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); - const traj = createModelTree(data, 'cif'); - return state.updateTree(createStructureTree(ctx, traj, false)); -}); - -function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') { - const parsed = format === 'cif' - ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) - : b.apply(StateTransforms.Model.TrajectoryFromPDB); - - return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); -} - -function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean): StateTree { - let root = b; - if (supportProps) { - root = root.apply(StateTransforms.Model.CustomModelProperties); - } - const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel); - complexRepresentation(ctx, structure); - - return root.getTree(); -} - -function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) { - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 })); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); -} - -export const CreateComplexRepresentation = StateAction.build({ - display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' }, - from: PluginStateObject.Molecule.Structure -})(({ ref, state }, ctx: PluginContext) => { - const root = state.build().to(ref); - complexRepresentation(ctx, root); - return state.updateTree(root.getTree()); -}); - -export const UpdateTrajectory = StateAction.build({ - display: { name: 'Update Trajectory' }, - params: { - action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), - by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) - } -})(({ params, state }) => { - const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model) - .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); - - const update = state.build(); - - if (params.action === 'reset') { - for (const m of models) { - update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, - () => ({ modelIndex: 0 })); - } - } else { - for (const m of models) { - const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); - if (!parent || !parent.obj) continue; - const traj = parent.obj as PluginStateObject.Molecule.Trajectory; - update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, - old => { - let modelIndex = (old.modelIndex + params.by!) % traj.data.length; - if (modelIndex < 0) modelIndex += traj.data.length; - return { modelIndex }; - }); - } - } - - return state.updateTree(update); -}); - -// +import { VolumeRepresentation3DHelpers } from '../transforms/representation'; export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> { private _list: { name: string, provider: DataFormatProvider<D> }[] = [] diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index db69594a3c13abea794a75df957abdd9dad58e19..c45e9fa015170f3646ea1590ea11db59976df29f 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { PluginCommands } from 'mol-plugin/command'; -import { UpdateTrajectory } from 'mol-plugin/state/actions/basic'; +import { UpdateTrajectory } from 'mol-plugin/state/actions/structure'; import { PluginUIComponent } from './base'; import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager'; diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index e0ee8590fa071f3d79fbd924b49c8bb4d3931282..578ba4bb0c3ea49f119fb7b5a0e03dbee274a8c6 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -174,19 +174,19 @@ export class CurrentObject extends PluginUIComponent { const cell = current.state.cells.get(ref)!; const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0; - const type = cell && cell.obj ? cell.obj.type : void 0; const transform = cell.transform; const def = transform.transformer.definition; - const actions = type ? current.state.actions.fromType(type) : []; + const actions = current.state.actions.fromCell(cell, this.plugin); return <> - <div className='msp-section-header'> + <div className='msp-current-header'> {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name} </div> - { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> } - {cell.status === 'ok' && - actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />) - } + {(parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} />} + {cell.status === 'ok' && <> + <div className='msp-section-header'>Actions</div> + {actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />)} + </>} </>; } } \ No newline at end of file diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts index 08de52da3adb116dde154358e8e738c871136f57..31b597fc2fd2e1f09193ee9375f6ecc580b65d63 100644 --- a/src/mol-state/action.ts +++ b/src/mol-state/action.ts @@ -10,6 +10,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { StateObject, StateObjectCell } from './object'; import { State } from './state'; import { StateTransformer } from './transformer'; +import { StateTransform } from './transform'; export { StateAction }; @@ -45,7 +46,7 @@ namespace StateAction { run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>, /** Test if the transform can be applied to a given node */ - isApplicable?(a: A, globalCtx: unknown): boolean + isApplicable?(a: A, aTransform: StateTransform<any, A, any>, globalCtx: unknown): boolean } export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> { @@ -69,6 +70,9 @@ namespace StateAction { from: def.from, display: def.display, params: def.params as StateTransformer.Definition<StateTransformer.From<T>, any, StateTransformer.Params<T>>['params'], + isApplicable: transformer.definition.isApplicable + ? (a, t, ctx) => transformer.definition.isApplicable!(a, ctx) + : void 0, run({ cell, state, params }) { const tree = state.build().to(cell.transform.ref).apply(transformer, params); return state.updateTree(tree) as Task<void>; @@ -80,7 +84,8 @@ namespace StateAction { export interface Type<A extends StateObject.Ctor, P extends { }> { from?: A | A[], params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>), - display?: string | { name: string, description?: string } + display?: string | { name: string, description?: string }, + isApplicable?: DefinitionBase<StateObject.From<A>, any, P>['isApplicable'] } export interface Root { @@ -106,6 +111,7 @@ namespace StateAction { : !!info.params ? info.params as any : void 0, + isApplicable: info.isApplicable, ...(typeof def === 'function' ? { run: def } : def) diff --git a/src/mol-state/action/manager.ts b/src/mol-state/action/manager.ts index 1fb97cab32737e5a2608b8fe621ab8cfbb8e2163..5a2f87b79cba66bc166831cb4d0a6bb805333759 100644 --- a/src/mol-state/action/manager.ts +++ b/src/mol-state/action/manager.ts @@ -5,7 +5,7 @@ */ import { StateAction } from '../action'; -import { StateObject } from '../object'; +import { StateObject, StateObjectCell } from '../object'; import { StateTransformer } from '../transformer'; export { StateActionManager } @@ -32,7 +32,31 @@ class StateActionManager { return this; } - fromType(type: StateObject.Type): ReadonlyArray<StateAction> { - return this.fromTypeIndex.get(type) || []; + fromCell(cell: StateObjectCell, ctx: unknown): ReadonlyArray<StateAction> { + const obj = cell.obj; + if (!obj) return []; + + const actions = this.fromTypeIndex.get(obj.type); + if (!actions) return []; + let hasTest = false; + for (const a of actions) { + if (a.definition.isApplicable) { + hasTest = true; + break; + } + } + if (!hasTest) return actions; + + const ret: StateAction[] = []; + for (const a of actions) { + if (a.definition.isApplicable) { + if (a.definition.isApplicable(obj, cell.transform, ctx)) { + ret.push(a); + } + } else { + ret.push(a); + } + } + return ret; } } \ No newline at end of file