diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts index 1421bec5e6fcc15f8bc0b8bf13ddc8d66d72dabb..d1b36b1199fb920c534f18f1aae9241077a97bf5 100644 --- a/src/mol-plugin/behavior.ts +++ b/src/mol-plugin/behavior.ts @@ -5,10 +5,14 @@ */ export * from './behavior/behavior' -import * as Data from './behavior/built-in/state' + +import * as State from './behavior/built-in/state' import * as Representation from './behavior/built-in/representation' +export const BuiltInPluginBehaviors = { + State, +} + export const PluginBehaviors = { - Data, Representation } \ No newline at end of file diff --git a/src/mol-plugin/behavior/built-in/state.ts b/src/mol-plugin/behavior/built-in/state.ts index 751a06c80f16ce04d20e4d62fda0e1a9d6ba53ab..195eb380cceb17c43ec978a7eaca05c010c93624 100644 --- a/src/mol-plugin/behavior/built-in/state.ts +++ b/src/mol-plugin/behavior/built-in/state.ts @@ -4,26 +4,51 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { PluginBehavior } from '../behavior'; import { PluginCommands } from '../../command'; +import { PluginContext } from '../../context'; -export const SetCurrentObject = PluginBehavior.create({ - name: 'set-current-data-object-behavior', - ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.SetCurrentObject, ({ state, ref }, ctx) => state.setCurrent(ref)), - display: { name: 'Set Current Handler', group: 'Data' } -}); - -export const Update = PluginBehavior.create({ - name: 'update-data-behavior', - ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.Update, ({ state, tree }, ctx) => ctx.runTask(state.update(tree))), - display: { name: 'Update Data Handler', group: 'Data' } -}); - -export const RemoveObject = PluginBehavior.create({ - name: 'remove-object-data-behavior', - ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.RemoveObject, ({ state, ref }, ctx) => { +export function SetCurrentObject(ctx: PluginContext) { + PluginCommands.State.SetCurrentObject.subscribe(ctx, ({ state, ref }) => state.setCurrent(ref)); +} + +export function Update(ctx: PluginContext) { + PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.update(tree))); +} + +export function ApplyAction(ctx: PluginContext) { + PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.apply(action.action, action.params, ref))); +} + +export function RemoveObject(ctx: PluginContext) { + PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => { const tree = state.tree.build().delete(ref).getTree(); return ctx.runTask(state.update(tree)); - }), - display: { name: 'Remove Object Handler', group: 'Data' } -}); \ No newline at end of file + }); +} + +// export const SetCurrentObject = PluginBehavior.create({ +// name: 'set-current-data-object-behavior', +// ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.SetCurrentObject, ({ state, ref }, ctx) => state.setCurrent(ref)), +// display: { name: 'Set Current Handler', group: 'Data' } +// }); + +// export const Update = PluginBehavior.create({ +// name: 'update-data-behavior', +// ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.Update, ({ state, tree }, ctx) => ctx.runTask(state.update(tree))), +// display: { name: 'Update Data Handler', group: 'Data' } +// }); + +// export const ApplyAction = PluginBehavior.create({ +// name: 'update-data-behavior', +// ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.Update, ({ state, tree }, ctx) => ctx.runTask(state.update(tree))), +// display: { name: 'Update Data Handler', group: 'Data' } +// }); + +// export const RemoveObject = PluginBehavior.create({ +// name: 'remove-object-data-behavior', +// ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.RemoveObject, ({ state, ref }, ctx) => { +// const tree = state.tree.build().delete(ref).getTree(); +// return ctx.runTask(state.update(tree)); +// }), +// display: { name: 'Remove Object Handler', group: 'Data' } +// }); \ No newline at end of file diff --git a/src/mol-plugin/command/command.ts b/src/mol-plugin/command/command.ts index a76186d5aeaaaae2753c8dd3228c80a035eda76a..fdf6f7f650cf3041cdb63fd7f1ae4edbd62605fe 100644 --- a/src/mol-plugin/command/command.ts +++ b/src/mol-plugin/command/command.ts @@ -111,14 +111,18 @@ namespace PluginCommand { } } + private executing = false; private async next() { - if (this.queue.count === 0) return; + if (this.queue.count === 0 || this.executing) return; const cmd = this.queue.removeFirst()!; const actions = this.subs.get(cmd.id); - if (!actions) return; + if (!actions) { + return; + } try { + this.executing = true; // TODO: should actions be called "asynchronously" ("setImmediate") instead? for (const a of actions) { await a(cmd.params); @@ -127,6 +131,7 @@ namespace PluginCommand { } catch (e) { cmd.reject(e); } finally { + this.executing = false; if (!this.disposing) this.next(); } } diff --git a/src/mol-plugin/command/state.ts b/src/mol-plugin/command/state.ts index dc0feadb81507765702142010b4bde84b4347682..c253c75ed799fc27c1bb8303c1e67a255aadc0c9 100644 --- a/src/mol-plugin/command/state.ts +++ b/src/mol-plugin/command/state.ts @@ -6,8 +6,10 @@ import { PluginCommand } from './command'; import { Transform, State } from 'mol-state'; +import { StateAction } from 'mol-state/action'; export const SetCurrentObject = PluginCommand<{ state: State, ref: Transform.Ref }>('ms-data', 'set-current-object'); +export const ApplyAction = PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>('ms-data', 'apply-action'); export const Update = PluginCommand<{ state: State, tree: State.Tree | State.Builder }>('ms-data', 'update'); // export const UpdateObject = PluginCommand<{ ref: Transform.Ref, params: any }>('ms-data', 'update-object'); diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 326d07368eaa5f4e54ebd736a85cf09fdd824b39..86c38c7fb36e95ec2063e44db12fdce2a6250172 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -10,13 +10,13 @@ import { StateTransforms } from './state/transforms'; import { PluginStateObject as SO } from './state/objects'; import { RxEventHelper } from 'mol-util/rx-event-helper'; import { PluginState } from './state'; -import { MolScriptBuilder } from 'mol-script/language/builder'; import { PluginCommand, PluginCommands } from './command'; import { Task } from 'mol-task'; import { merge } from 'rxjs'; -import { PluginBehaviors } from './behavior'; +import { PluginBehaviors, BuiltInPluginBehaviors } from './behavior'; import { Loci, EmptyLoci } from 'mol-model/loci'; import { Representation } from 'mol-repr'; +import { CreateStructureFromPDBe } from './state/actions/basic'; export class PluginContext { private disposed = false; @@ -69,7 +69,7 @@ export class PluginContext { } async runTask<T>(task: Task<T>) { - return await task.run(p => console.log(p), 250); + return await task.run(p => console.log(p.root.progress.message), 250); } dispose() { @@ -81,12 +81,16 @@ export class PluginContext { this.disposed = true; } - async _test_initBehaviours() { + private initBuiltInBehavior() { + BuiltInPluginBehaviors.State.ApplyAction(this); + BuiltInPluginBehaviors.State.RemoveObject(this); + BuiltInPluginBehaviors.State.SetCurrentObject(this); + BuiltInPluginBehaviors.State.Update(this); + } + + async _test_initBehaviors() { const tree = this.state.behavior.tree.build() - .toRoot().apply(PluginBehaviors.Data.SetCurrentObject, { ref: PluginBehaviors.Data.SetCurrentObject.id }) - .and().toRoot().apply(PluginBehaviors.Data.Update, { ref: PluginBehaviors.Data.Update.id }) - .and().toRoot().apply(PluginBehaviors.Data.RemoveObject, { ref: PluginBehaviors.Data.RemoveObject.id }) - .and().toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas, { ref: PluginBehaviors.Representation.AddRepresentationToCanvas.id }) + .toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas, { ref: PluginBehaviors.Representation.AddRepresentationToCanvas.id }) .and().toRoot().apply(PluginBehaviors.Representation.HighlightLoci, { ref: PluginBehaviors.Representation.HighlightLoci.id }) .and().toRoot().apply(PluginBehaviors.Representation.SelectLoci, { ref: PluginBehaviors.Representation.SelectLoci.id }) .getTree(); @@ -94,6 +98,12 @@ export class PluginContext { await this.runTask(this.state.behavior.update(tree)); } + _test_initDataActions() { + this.state.data.actions + .add(CreateStructureFromPDBe) + .add(StateTransforms.Data.Download.toAction()); + } + applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) { const tree = state.tree.build().to(a).apply(transformer, params); return PluginCommands.State.Update.dispatch(this, { state, tree }); @@ -104,32 +114,8 @@ export class PluginContext { return PluginCommands.State.Update.dispatch(this, { state, tree }); } - _test_createState(url: string) { - const b = this.state.data.tree.build(); - - const query = MolScriptBuilder.struct.generator.atomGroups({ - // 'atom-test': MolScriptBuilder.core.rel.eq([ - // MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), - // MolScriptBuilder.es('C') - // ]), - 'residue-test': MolScriptBuilder.core.rel.eq([ - MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), - 'ALA' - ]) - }); - - const newTree = b.toRoot() - .apply(StateTransforms.Data.Download, { url }) - .apply(StateTransforms.Data.ParseCif) - .apply(StateTransforms.Model.ParseTrajectoryFromMmCif, {}, { ref: 'trajectory' }) - .apply(StateTransforms.Model.CreateModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' }) - .apply(StateTransforms.Model.CreateStructureFromModel, { }, { ref: 'structure' }) - .apply(StateTransforms.Model.CreateStructureAssembly) - .apply(StateTransforms.Model.CreateStructureSelection, { query, label: 'ALA residues' }) - .apply(StateTransforms.Visuals.CreateStructureRepresentation) - .getTree(); - - this.runTask(this.state.data.update(newTree)); + _test_createState(id: string) { + this.runTask(this.state.data.apply(CreateStructureFromPDBe, { id })); } private initEvents() { @@ -159,30 +145,12 @@ export class PluginContext { this.canvas3d.requestDraw(true); } - async _test_nextModel() { - const traj = this.state.data.select('trajectory')[0].obj as SO.Molecule.Trajectory; - //const modelIndex = (this.state.data.select('model')[0].transform.params as CreateModelFromTrajectory.Params).modelIndex; - const newTree = this.state.data.build().to('model').update( - StateTransforms.Model.CreateModelFromTrajectory, - old => ({ modelIndex: (old.modelIndex + 1) % traj.data.length })) - .getTree(); - // const newTree = StateTree.updateParams(this.state.data.tree, 'model', { modelIndex: (modelIndex + 1) % traj.data.length }); - await this.runTask(this.state.data.update(newTree)); - // this.viewer.requestDraw(true); - } - - _test_playModels() { - const update = async () => { - await this._test_nextModel(); - setTimeout(update, 1000 / 15); - } - update(); - } - constructor() { this.initEvents(); + this.initBuiltInBehavior(); - this._test_initBehaviours(); + this._test_initBehaviors(); + this._test_initDataActions(); } // logger = ; diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index 11efaeae59cd1b93481163252f7925f02a1372a5..6d4633670302a7fba8eb4c9310d80f5422b0fd81 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/basic.ts @@ -4,4 +4,87 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -// TODO: basic actions like "download and create default representation" \ No newline at end of file +import { StateAction } from 'mol-state/action'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { StateSelection } from 'mol-state/state/selection'; + +export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root, void, { id: string }>({ + from: [PluginStateObject.Root], + display: { + name: 'Entry from PDBe', + description: 'Download a structure from PDBe and create its default Assembly and visual' + }, + params: { + default: () => ({ id: '1grm' }), + controls: () => ({ + id: PD.Text('PDB id', '', '1grm'), + }) + }, + apply({ params, state }) { + const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`; + const b = state.build(); + + // const query = MolScriptBuilder.struct.generator.atomGroups({ + // // 'atom-test': MolScriptBuilder.core.rel.eq([ + // // MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), + // // MolScriptBuilder.es('C') + // // ]), + // 'residue-test': MolScriptBuilder.core.rel.eq([ + // MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), + // 'ALA' + // ]) + // }); + + const newTree = b.toRoot() + .apply(StateTransforms.Data.Download, { url }) + .apply(StateTransforms.Data.ParseCif) + .apply(StateTransforms.Model.ParseTrajectoryFromMmCif, {}) + .apply(StateTransforms.Model.CreateModelFromTrajectory, { modelIndex: 0 }) + .apply(StateTransforms.Model.CreateStructureFromModel, { }) + .apply(StateTransforms.Model.CreateStructureAssembly) + // .apply(StateTransforms.Model.CreateStructureSelection, { query, label: 'ALA residues' }) + .apply(StateTransforms.Visuals.CreateStructureRepresentation) + .getTree(); + + return state.update(newTree); + } +}); + +export const UpdateTrajectory = StateAction.create<PluginStateObject.Root, void, { action: 'advance' | 'reset', by?: number }>({ + from: [], + display: { + name: 'Entry from PDBe', + description: 'Download a structure from PDBe and create its default Assembly and visual' + }, + params: { + default: () => ({ action: 'reset', by: 1 }) + }, + apply({ params, state }) { + const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model).filter(c => c.transform.transformer === StateTransforms.Model.CreateModelFromTrajectory)); + + const update = state.build(); + + if (params.action === 'reset') { + for (const m of models) { + update.to(m.transform.ref).update(StateTransforms.Model.CreateModelFromTrajectory, + () => ({ 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.CreateModelFromTrajectory, + old => { + let modelIndex = (old.modelIndex + params.by!) % traj.data.length; + if (modelIndex < 0) modelIndex += traj.data.length; + return { modelIndex }; + }); + } + } + + return state.update(update); + } +}); \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 4d2a53b4672237a55e3ee583d6f0053bd37fe71b..58af7db25378e42d4efcc6503b35af7653d4e837 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -62,7 +62,6 @@ const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajec }, isApplicable: a => a.data.length > 0, apply({ a, params }) { - console.log('parans', params); if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); const model = a.data[params.modelIndex]; const label = { label: `Model ${model.modelNum}` }; diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 1e6e881ad83b7ea557fea06b74575732662c8a99..b1a813abe967939d1a6571a12ad8034840caa174 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -6,18 +6,15 @@ import * as React from 'react'; import { PluginContext } from '../context'; -import { Transform, Transformer, State } from 'mol-state'; +import { Transform, State } from 'mol-state'; import { ParametersComponent } from 'mol-app/component/parameters'; +import { StateAction } from 'mol-state/action'; +import { PluginCommands } from 'mol-plugin/command'; +import { UpdateTrajectory } from 'mol-plugin/state/actions/basic'; export class Controls extends React.Component<{ plugin: PluginContext }, { id: string }> { state = { id: '1grm' }; - private createState = () => { - const url = `http://www.ebi.ac.uk/pdbe/static/entry/${this.state.id.toLowerCase()}_updated.cif`; - // const url = `https://webchem.ncbr.muni.cz/CoordinateServer/${this.state.id.toLowerCase()}/full` - this.props.plugin._test_createState(url); - } - private _snap: any = void 0; private getSnapshot = () => { this._snap = this.props.plugin.state.getSnapshot(); @@ -30,11 +27,7 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s render() { return <div> - <input type='text' defaultValue={this.state.id} onChange={e => this.setState({ id: e.currentTarget.value })} /> - <button onClick={this.createState}>Create State</button><br/> - <button onClick={() => this.props.plugin._test_centerView()}>Center View</button><br/> - <button onClick={() => this.props.plugin._test_nextModel()}>Next Model</button><br/> - <button onClick={() => this.props.plugin._test_playModels()}>Play Models</button><br/> + <button onClick={() => this.props.plugin._test_centerView()}>Center View</button><br /> <hr /> <button onClick={this.getSnapshot}>Get Snapshot</button> <button onClick={this.setSnapshot}>Set Snapshot</button> @@ -42,31 +35,57 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s } } -export class _test_CreateTransform extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, state: State, transformer: Transformer }, { params: any }> { + +export class _test_TrajectoryControls extends React.Component<{ plugin: PluginContext }> { + render() { + return <div> + <b>Trajectory: </b> + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { + state: this.props.plugin.state.data, + action: UpdateTrajectory.create({ action: 'advance', by: -1 }) + })}><<</button> + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { + state: this.props.plugin.state.data, + action: UpdateTrajectory.create({ action: 'reset' }) + })}>Reset</button> + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { + state: this.props.plugin.state.data, + action: UpdateTrajectory.create({ action: 'advance', by: +1 }) + })}>>></button><br /> + </div> + } +} + +export class _test_ApplyAction extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, state: State, action: StateAction }, { params: any }> { private getObj() { const obj = this.props.state.cells.get(this.props.nodeRef)!; return obj; } private getDefaultParams() { - const p = this.props.transformer.definition.params; - if (!p || !p.default) return { }; + const p = this.props.action.definition.params; + if (!p || !p.default) return {}; const obj = this.getObj(); - if (!obj.obj) return { }; + if (!obj.obj) return {}; return p.default(obj.obj, this.props.plugin); } private getParamDef() { - const p = this.props.transformer.definition.params; - if (!p || !p.controls) return { }; + const p = this.props.action.definition.params; + if (!p || !p.controls) return {}; const obj = this.getObj(); - if (!obj.obj) return { }; + if (!obj.obj) return {}; return p.controls(obj.obj, this.props.plugin); } private create() { - console.log(this.props.transformer.definition.name, this.state.params); - this.props.plugin.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params); + console.log('Apply Action', this.state.params); + PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { + state: this.props.state, + action: this.props.action.create(this.state.params), + ref: this.props.nodeRef + }); + // this.props.plugin.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params); } state = { params: this.getDefaultParams() } @@ -78,10 +97,10 @@ export class _test_CreateTransform extends React.Component<{ plugin: PluginConte return <div />; } - const t = this.props.transformer; + const action = this.props.action; - return <div key={`${this.props.nodeRef} ${this.props.transformer.id}`}> - <div style={{ borderBottom: '1px solid #999'}}>{(t.definition.display && t.definition.display.name) || t.definition.name}</div> + return <div key={`${this.props.nodeRef} ${this.props.action.id}`}> + <div style={{ borderBottom: '1px solid #999' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div> <ParametersComponent params={this.getParamDef()} values={this.state.params as any} onChange={(k, v) => { this.setState({ params: { ...this.state.params, [k]: v } }); }} /> @@ -97,7 +116,7 @@ export class _test_UpdateTransform extends React.Component<{ plugin: PluginConte private getDefParams() { const cell = this.getCell(); - if (!cell) return { }; + if (!cell) return {}; return cell.transform.params; } @@ -136,8 +155,8 @@ export class _test_UpdateTransform extends React.Component<{ plugin: PluginConte const tr = transform.transformer; - return <div key={`${this.props.nodeRef} ${tr.id}`}> - <div style={{ borderBottom: '1px solid #999'}}>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</div> + return <div key={`${this.props.nodeRef} ${tr.id}`} style={{ marginBottom: '10ox' }}> + <div style={{ borderBottom: '1px solid #999' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</h3></div> <ParametersComponent params={params} values={this.state.params as any} onChange={(k, v) => { this.setState({ params: { ...this.state.params, [k]: v } }); }} /> diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index ccb202b3f8868abc8ea84547b3241c5b8e61d060..cbd04d53d5f587d416009fa8c976aed695fa2f6b 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -8,26 +8,27 @@ import * as React from 'react'; import { PluginContext } from '../context'; import { StateTree } from './state-tree'; import { Viewport } from './viewport'; -import { Controls, _test_CreateTransform, _test_UpdateTransform } from './controls'; -import { Transformer } from 'mol-state'; +import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls } from './controls'; // TODO: base object with subscribe helpers, separate behavior list etc export class Plugin extends React.Component<{ plugin: PluginContext }, { }> { render() { return <div style={{ position: 'absolute', width: '100%', height: '100%', fontFamily: 'monospace' }}> - <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll' }}> - <h3>Data</h3> + <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll', padding: '10px' }}> <StateTree plugin={this.props.plugin} state={this.props.plugin.state.data} /> - <hr /> - <_test_CurrentObject plugin={this.props.plugin} /> <h3>Behaviors</h3> <StateTree plugin={this.props.plugin} state={this.props.plugin.state.behavior} /> </div> - <div style={{ position: 'absolute', left: '350px', right: '250px', height: '100%' }}> + <div style={{ position: 'absolute', left: '350px', right: '300px', height: '100%' }}> <Viewport plugin={this.props.plugin} /> + <div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}> + <_test_TrajectoryControls {...this.props} /> + </div> </div> - <div style={{ position: 'absolute', width: '250px', right: '0', height: '100%' }}> + <div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px' }}> + <_test_CurrentObject plugin={this.props.plugin} /> + <hr /> <Controls plugin={this.props.plugin} /> </div> </div>; @@ -41,24 +42,25 @@ export class _test_CurrentObject extends React.Component<{ plugin: PluginContext } render() { const current = this.props.plugin.behaviors.state.data.currentObject.value; + const ref = current.ref; // const n = this.props.plugin.state.data.tree.nodes.get(ref)!; const obj = this.props.plugin.state.data.cells.get(ref)!; const type = obj && obj.obj ? obj.obj.type : void 0; - const transforms = type - ? Transformer.fromType(type) + const actions = type + ? current.state.actions.fromType(type) : [] return <div> - Current Ref: {ref} <hr /> - <h3>Update</h3> + <h3>Update {ref}</h3> <_test_UpdateTransform key={`${ref} update`} plugin={this.props.plugin} state={current.state} nodeRef={ref} /> <hr /> <h3>Create</h3> { - transforms.map((t, i) => <_test_CreateTransform key={`${t.id} ${ref} ${i}`} plugin={this.props.plugin} state={current.state} transformer={t} nodeRef={ref} />) + actions.map((act, i) => <_test_ApplyAction key={`${act.id} ${ref} ${i}`} + plugin={this.props.plugin} state={current.state} action={act} nodeRef={ref} />) } </div>; } diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts index abc53999e6bdd2a4caf32a9e6f20a1cd067641e2..fc86acbea2c427251005b3083a8a54ebdf757d1c 100644 --- a/src/mol-state/action.ts +++ b/src/mol-state/action.ts @@ -14,6 +14,7 @@ import { Transformer } from './transformer'; export { StateAction }; interface StateAction<A extends StateObject = StateObject, T = any, P = unknown> { + create(params: P): StateAction.Instance, readonly id: UUID, readonly definition: StateAction.Definition<A, T, P> } @@ -24,6 +25,11 @@ namespace StateAction { export type ReType<T extends StateAction<any, any, any>> = T extends StateAction<any, infer T, any> ? T : unknown; export type ControlsFor<Props> = { [P in keyof Props]?: PD.Any } + export interface Instance { + action: StateAction, + params: any + } + export interface ApplyParams<A extends StateObject = StateObject, P = unknown> { cell: StateObjectCell, a: A, @@ -47,7 +53,12 @@ namespace StateAction { } export function create<A extends StateObject, T, P>(definition: Definition<A, T, P>): StateAction<A, T, P> { - return { id: UUID.create(), definition }; + const action: StateAction<A, T, P> = { + create(params) { return { action, params }; }, + id: UUID.create(), + definition + }; + return action; } export function fromTransformer<T extends Transformer>(transformer: T) { diff --git a/src/mol-state/action/manager.ts b/src/mol-state/action/manager.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..373e68d0c95f6fe782e03d85edebb7feb551bd54 100644 --- a/src/mol-state/action/manager.ts +++ b/src/mol-state/action/manager.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateAction } from 'mol-state/action'; +import { StateObject } from '../object'; + +export { StateActionManager } + +class StateActionManager { + private actions: Map<StateAction['id'], StateAction> = new Map(); + private fromTypeIndex = new Map<StateObject.Type, StateAction[]>(); + + add(action: StateAction) { + if (this.actions.has(action.id)) return this; + + this.actions.set(action.id, action); + + for (const t of action.definition.from) { + if (this.fromTypeIndex.has(t.type)) { + this.fromTypeIndex.get(t.type)!.push(action); + } else { + this.fromTypeIndex.set(t.type, [action]); + } + } + + return this; + } + + fromType(type: StateObject.Type): ReadonlyArray<StateAction> { + return this.fromTypeIndex.get(type) || []; + } +} \ No newline at end of file diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index a9db3b482dcc305c013d0d290c1886769464dcb2..462b2d2e59005e5a6de4bb86c37f55d4f738abc1 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -14,6 +14,7 @@ import { StateSelection } from './state/selection'; import { RxEventHelper } from 'mol-util/rx-event-helper'; import { StateTreeBuilder } from './tree/builder'; import { StateAction } from './action'; +import { StateActionManager } from './action/manager'; export { State } @@ -42,6 +43,8 @@ class State { currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef }) }; + readonly actions = new StateActionManager(); + get tree() { return this._tree; } get current() { return this._current; } @@ -81,8 +84,15 @@ class State { return StateSelection.select(selector(StateSelection.Generators), this) } - apply(action: StateAction, ref: Transform.Ref) { - + /** Is no ref is specified, apply to root */ + apply<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> { + return Task.create('Apply Action', ctx => { + const cell = this.cells.get(ref); + if (!cell) throw new Error(`'${ref}' does not exist.`); + if (cell.status !== 'ok') throw new Error(`Action cannot be applied to a cell with status '${cell.status}'`); + + return runTask(action.definition.apply({ cell, a: cell.obj!, params, state: this }, this.globalContext), ctx); + }); } update(tree: StateTree | StateTreeBuilder): Task<void> { diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts index 894506d6a38a0cd8ee2cdbc75c4e0167fdb5b326..7d0caa949a409281859b85076ecee3674be9c5b6 100644 --- a/src/mol-state/state/selection.ts +++ b/src/mol-state/state/selection.ts @@ -87,6 +87,23 @@ namespace StateSelection { } export function byValue(...objects: StateObjectCell[]) { return build(() => (state: State) => objects); } + + export function rootsOfType(type: StateObject.Ctor) { + return build(() => state => { + const ctx = { roots: [] as StateObjectCell[], cells: state.cells, type: type.type }; + StateTree.doPreOrder(state.tree, state.tree.root, ctx, _findRootsOfType); + return ctx.roots; + }); + } + + function _findRootsOfType(n: Transform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) { + const cell = s.cells.get(n.ref); + if (cell && cell.obj && cell.obj.type === s.type) { + s.roots.push(cell); + return false; + } + return true; + } } registerModifier('flatMap', flatMap); diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts index ab6dfe74102a3a39dd6e5698a41b75cc6ee162b3..79e7511c7288ae156a839587bea893810d79ba7d 100644 --- a/src/mol-state/tree/builder.ts +++ b/src/mol-state/tree/builder.ts @@ -9,6 +9,7 @@ import { TransientTree } from './transient'; import { StateObject } from '../object'; import { Transform } from '../transform'; import { Transformer } from '../transformer'; +import { shallowEqual } from 'mol-util'; export { StateTreeBuilder } @@ -54,6 +55,15 @@ namespace StateTreeBuilder { } else { params = paramsOrTransformer; } + + if (old.transformer.definition.params && old.transformer.definition.params.areEqual) { + if (old.transformer.definition.params.areEqual(old.params, params)) return this.root; + } else { + if (shallowEqual(old.params, params)) { + return this.root; + } + } + this.state.tree.set(Transform.updateParams(old, params)); return this.root; }