diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 06371c93095b95784f23b382ed701ebbb7927e23..10014d4da34a0f6b175eba8d7df49271e6263e7b 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -7,6 +7,7 @@ import { PluginCommands } from '../../command'; import { PluginContext } from '../../context'; import { StateTree, Transform, State } from 'mol-state'; +import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots'; export function registerDefault(ctx: PluginContext) { SetCurrentObject(ctx); @@ -15,6 +16,7 @@ export function registerDefault(ctx: PluginContext) { RemoveObject(ctx); ToggleExpanded(ctx); ToggleVisibility(ctx); + Snapshots(ctx); } export function SetCurrentObject(ctx: PluginContext) { @@ -50,4 +52,24 @@ function setVisibility(state: State, root: Transform.Ref, value: boolean) { function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State, value: boolean }) { ctx.state.updateCellState(t.ref, { isHidden: ctx.value }); +} + +export function Snapshots(ctx: PluginContext) { + PluginCommands.State.Snapshots.Clear.subscribe(ctx, () => { + ctx.state.snapshots.clear(); + }); + + PluginCommands.State.Snapshots.Remove.subscribe(ctx, ({ id }) => { + ctx.state.snapshots.remove(id); + }); + + PluginCommands.State.Snapshots.Add.subscribe(ctx, ({ name, description }) => { + const entry = PluginStateSnapshotManager.Entry(name || new Date().toLocaleTimeString(), ctx.state.getSnapshot(), description); + ctx.state.snapshots.add(entry); + }); + + PluginCommands.State.Snapshots.Apply.subscribe(ctx, ({ id }) => { + const e = ctx.state.snapshots.getEntry(id); + return ctx.state.setSnapshot(e.snapshot); + }); } \ No newline at end of file diff --git a/src/mol-plugin/command/state.ts b/src/mol-plugin/command/state.ts index c8e87bc7a2b0bd01136e959ecee9608cf17debbc..ed9ca61c78bf5c0ee356a3f2fd8342f94ec96715 100644 --- a/src/mol-plugin/command/state.ts +++ b/src/mol-plugin/command/state.ts @@ -18,4 +18,11 @@ export const RemoveObject = PluginCommand<{ state: State, ref: Transform.Ref }>( export const ToggleExpanded = PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }); -export const ToggleVisibility = PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }); \ No newline at end of file +export const ToggleVisibility = PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }); + +export const Snapshots = { + Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }), + Remove: PluginCommand<{ id: string }>({ isImmediate: true }), + Apply: PluginCommand<{ id: string }>({ isImmediate: true }), + Clear: PluginCommand<{ }>({ isImmediate: true }), +} \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 83e603b57e411f87a5ac59f4a8d3115cb1453e1a..aa7551cd000c9886591715fd0f19f8687f6d8a31 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -29,7 +29,8 @@ export class PluginContext { state: { data: this.state.data.events, behavior: this.state.behavior.events, - cameraSnapshots: this.state.cameraSnapshots.events + cameraSnapshots: this.state.cameraSnapshots.events, + snapshots: this.state.snapshots.events, } }; diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index ecff2513dfceaf0b63af797e681abd25ff33bee9..15dbf204e714d1908ec5be732d5db8769a7a92a5 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -9,13 +9,16 @@ import { PluginStateObject as SO } from './state/objects'; import { Camera } from 'mol-canvas3d/camera'; import { PluginBehavior } from './behavior'; import { CameraSnapshotManager } from './state/camera'; +import { PluginStateSnapshotManager } from './state/snapshots'; export { PluginState } class PluginState { readonly data: State; readonly behavior: State; - readonly cameraSnapshots: CameraSnapshotManager = new CameraSnapshotManager(); + readonly cameraSnapshots = new CameraSnapshotManager(); + + readonly snapshots = new PluginStateSnapshotManager(); getSnapshot(): PluginState.Snapshot { return { diff --git a/src/mol-plugin/state/snapshots.ts b/src/mol-plugin/state/snapshots.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5ff217af4abba33d067a1a43126fce16d777c45 --- /dev/null +++ b/src/mol-plugin/state/snapshots.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { OrderedMap } from 'immutable'; +import { UUID } from 'mol-util'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; +import { PluginState } from '../state'; + +export { PluginStateSnapshotManager } + +class PluginStateSnapshotManager { + private ev = RxEventHelper.create(); + private _entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable(); + + readonly events = { + changed: this.ev() + }; + + get entries() { return this._entries; } + + getEntry(id: string) { + return this._entries.get(id); + } + + remove(id: string) { + if (!this._entries.has(id)) return; + this._entries.delete(id); + this.events.changed.next(); + } + + add(e: PluginStateSnapshotManager.Entry) { + this._entries.set(e.id, e); + this.events.changed.next(); + } + + clear() { + if (this._entries.size === 0) return; + this._entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable(); + this.events.changed.next(); + } + + dispose() { + this.ev.dispose(); + } +} + +namespace PluginStateSnapshotManager { + export interface Entry { + id: UUID, + name: string, + description?: string, + snapshot: PluginState.Snapshot + } + + export function Entry(name: string, snapshot: PluginState.Snapshot, description?: string): Entry { + return { id: UUID.create22(), name, snapshot, description }; + } + + export interface StateSnapshot { + entries: Entry[] + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/camera.tsx b/src/mol-plugin/ui/camera.tsx new file mode 100644 index 0000000000000000000000000000000000000000..07a7b795b9325d1d03e6c29a174410cb7566c88b --- /dev/null +++ b/src/mol-plugin/ui/camera.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginCommands } from 'mol-plugin/command'; +import * as React from 'react'; +import { PluginComponent } from './base'; + +export class CameraSnapshots extends PluginComponent<{ }, { }> { + render() { + return <div> + <h3>Camera Snapshots</h3> + <CameraSnapshotControls /> + <CameraSnapshotList /> + </div>; + } +} + +class CameraSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> { + state = { name: '', description: '' }; + + add = () => { + PluginCommands.Camera.Snapshots.Add.dispatch(this.plugin, this.state); + this.setState({ name: '', description: '' }) + } + + clear = () => { + PluginCommands.Camera.Snapshots.Clear.dispatch(this.plugin, {}); + } + + render() { + return <div> + <input type='text' value={this.state.name} placeholder='Name...' style={{ width: '33%', display: 'block', float: 'left' }} onChange={e => this.setState({ name: e.target.value })} /> + <input type='text' value={this.state.description} placeholder='Description...' style={{ width: '67%', display: 'block' }} onChange={e => this.setState({ description: e.target.value })} /> + <button style={{ float: 'right' }} onClick={this.clear}>Clear</button> + <button onClick={this.add}>Add</button> + </div>; + } +} + +class CameraSnapshotList extends PluginComponent<{ }, { }> { + componentDidMount() { + this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate()); + } + + apply(id: string) { + return () => PluginCommands.Camera.Snapshots.Apply.dispatch(this.plugin, { id }); + } + + remove(id: string) { + return () => { + PluginCommands.Camera.Snapshots.Remove.dispatch(this.plugin, { id }); + } + } + + render() { + return <ul style={{ listStyle: 'none' }}> + {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}> + <button onClick={this.apply(e!.id)}>Set</button> + {e!.name} <small>{e!.description}</small> + <button onClick={this.remove(e!.id)} style={{ float: 'right' }}>X</button> + </li>)} + </ul>; + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 39d97afe1ed6563964d8262a7883093865eaf97e..660386425e24e8e80e1a4606f04754b11a16d78b 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -13,23 +13,10 @@ import { UpdateTrajectory } from 'mol-plugin/state/actions/basic'; import { PluginComponent } from './base'; export class Controls extends PluginComponent<{ }, { }> { - state = { id: '1grm' }; - - private _snap: any = void 0; - private getSnapshot = () => { - this._snap = this.plugin.state.getSnapshot(); - console.log(btoa(JSON.stringify(this._snap))); - } - private setSnapshot = () => { - if (!this._snap) return; - this.plugin.state.setSnapshot(this._snap); - } - render() { - return <div> - <button onClick={this.getSnapshot}>Get Snapshot</button> - <button onClick={this.setSnapshot}>Set Snapshot</button> - </div>; + return <> + + </>; } } @@ -163,66 +150,4 @@ export class _test_UpdateTransform extends PluginComponent<{ state: State, nodeR <button onClick={() => this.update()} style={{ width: '100%' }}>Update</button> </div> } -} - -export class CameraSnapshots extends PluginComponent<{ }, { }> { - render() { - return <div> - <h3>Camera Snapshots</h3> - <CameraSnapshotControls /> - <CameraSnapshotList /> - </div>; - } -} - -class CameraSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> { - componentDidMount() { - this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate()); - } - - state = { name: '', description: '' }; - - add = () => { - PluginCommands.Camera.Snapshots.Add.dispatch(this.plugin, this.state); - this.setState({ name: '', description: '' }) - } - - clear = () => { - PluginCommands.Camera.Snapshots.Clear.dispatch(this.plugin, {}); - } - - render() { - return <div> - <input type='text' value={this.state.name} placeholder='Name...' style={{ width: '33%', display: 'block', float: 'left' }} onChange={e => this.setState({ name: e.target.value })} /> - <input type='text' value={this.state.description} placeholder='Description...' style={{ width: '67%', display: 'block' }} onChange={e => this.setState({ description: e.target.value })} /> - <button style={{ float: 'right' }} onClick={this.clear}>Clear</button> - <button onClick={this.add}>Add</button> - </div>; - } -} - -class CameraSnapshotList extends PluginComponent<{ }, { }> { - componentDidMount() { - this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate()); - } - - apply(id: string) { - return () => PluginCommands.Camera.Snapshots.Apply.dispatch(this.plugin, { id }); - } - - remove(id: string) { - return () => { - PluginCommands.Camera.Snapshots.Remove.dispatch(this.plugin, { id }); - } - } - - render() { - return <ul style={{ listStyle: 'none' }}> - {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}> - <button onClick={this.apply(e!.id)}>Set</button> - {e!.name} <small>{e!.description}</small> - <button onClick={this.remove(e!.id)} style={{ float: 'right' }}>X</button> - </li>)} - </ul>; - } } \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 0d81076604e48056636b5d16e2c6490561f665b8..483ef6ee8c08d57e2b8073e4f6a2763fc3b66f11 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -8,10 +8,12 @@ import * as React from 'react'; import { PluginContext } from '../context'; import { StateTree } from './state-tree'; import { Viewport, ViewportControls } from './viewport'; -import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls, CameraSnapshots } from './controls'; +import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls } from './controls'; import { PluginComponent, PluginReactContext } from './base'; import { merge } from 'rxjs'; import { State } from 'mol-state'; +import { CameraSnapshots } from './camera'; +import { StateSnapshots } from './state'; export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { render() { @@ -29,12 +31,14 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { </div> <ViewportControls /> </div> - <div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px' }}> + <div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px', overflowY: 'scroll' }}> <_test_CurrentObject /> <hr /> <Controls /> <hr /> <CameraSnapshots /> + <hr /> + <StateSnapshots /> </div> </div> </PluginReactContext.Provider>; diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx new file mode 100644 index 0000000000000000000000000000000000000000..40ad8905684573a0678dccf4d16e2ddf88129726 --- /dev/null +++ b/src/mol-plugin/ui/state.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginCommands } from 'mol-plugin/command'; +import * as React from 'react'; +import { PluginComponent } from './base'; + +export class StateSnapshots extends PluginComponent<{ }, { }> { + render() { + return <div> + <h3>State Snapshots</h3> + <StateSnapshotControls /> + <StateSnapshotList /> + </div>; + } +} + +class StateSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> { + state = { name: '', description: '' }; + + add = () => { + PluginCommands.State.Snapshots.Add.dispatch(this.plugin, this.state); + this.setState({ name: '', description: '' }) + } + + clear = () => { + PluginCommands.State.Snapshots.Clear.dispatch(this.plugin, {}); + } + + render() { + return <div> + <input type='text' value={this.state.name} placeholder='Name...' style={{ width: '33%', display: 'block', float: 'left' }} onChange={e => this.setState({ name: e.target.value })} /> + <input type='text' value={this.state.description} placeholder='Description...' style={{ width: '67%', display: 'block' }} onChange={e => this.setState({ description: e.target.value })} /> + <button style={{ float: 'right' }} onClick={this.clear}>Clear</button> + <button onClick={this.add}>Add</button> + </div>; + } +} + +class StateSnapshotList extends PluginComponent<{ }, { }> { + componentDidMount() { + this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); + } + + apply(id: string) { + return () => PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id }); + } + + remove(id: string) { + return () => { + PluginCommands.State.Snapshots.Remove.dispatch(this.plugin, { id }); + } + } + + render() { + return <ul style={{ listStyle: 'none' }}> + {this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}> + <button onClick={this.apply(e!.id)}>Set</button> + {e!.name} <small>{e!.description}</small> + <button onClick={this.remove(e!.id)} style={{ float: 'right' }}>X</button> + </li>)} + </ul>; + } +} \ No newline at end of file