diff --git a/src/mol-plugin/behavior/static/camera.ts b/src/mol-plugin/behavior/static/camera.ts index 1199b2fe4f89f806621bdef4c4b4df0ce058e1cb..48a47e214091910efa7ebf44416b7ffee241502f 100644 --- a/src/mol-plugin/behavior/static/camera.ts +++ b/src/mol-plugin/behavior/static/camera.ts @@ -7,9 +7,12 @@ import { PluginContext } from 'mol-plugin/context'; import { PluginCommands } from 'mol-plugin/command'; import { PluginStateObject as SO } from '../../state/objects'; +import { CameraSnapshotManager } from 'mol-plugin/state/camera'; export function registerDefault(ctx: PluginContext) { Reset(ctx); + SetSnapshot(ctx); + Snapshots(ctx); } export function Reset(ctx: PluginContext) { @@ -24,4 +27,31 @@ export function Reset(ctx: PluginContext) { // TODO // ctx.canvas3d.resetCamera(); }) +} + +export function SetSnapshot(ctx: PluginContext) { + PluginCommands.Camera.SetSnapshot.subscribe(ctx, ({ snapshot }) => { + ctx.canvas3d.camera.setState(snapshot); + ctx.canvas3d.requestDraw(); + }) +} + +export function Snapshots(ctx: PluginContext) { + PluginCommands.Camera.Snapshots.Clear.subscribe(ctx, () => { + ctx.state.cameraSnapshots.clear(); + }); + + PluginCommands.Camera.Snapshots.Remove.subscribe(ctx, ({ id }) => { + ctx.state.cameraSnapshots.remove(id); + }); + + PluginCommands.Camera.Snapshots.Add.subscribe(ctx, ({ name, description }) => { + const entry = CameraSnapshotManager.Entry(name || new Date().toLocaleTimeString(), ctx.canvas3d.camera.getSnapshot(), description); + ctx.state.cameraSnapshots.add(entry); + }); + + PluginCommands.Camera.Snapshots.Apply.subscribe(ctx, ({ id }) => { + const e = ctx.state.cameraSnapshots.getEntry(id); + return PluginCommands.Camera.SetSnapshot.dispatch(ctx, { snapshot: e.snapshot }); + }); } \ No newline at end of file diff --git a/src/mol-plugin/command/camera.ts b/src/mol-plugin/command/camera.ts index 22a07d503e5f729527727ac90c06c756586cda11..0020d13354819b81f82013011e4c26e4f455312a 100644 --- a/src/mol-plugin/command/camera.ts +++ b/src/mol-plugin/command/camera.ts @@ -5,5 +5,14 @@ */ import { PluginCommand } from './command'; +import { Camera } from 'mol-canvas3d/camera'; -export const Reset = PluginCommand<{}>({ isImmediate: true }); \ No newline at end of file +export const Reset = PluginCommand<{}>({ isImmediate: true }); +export const SetSnapshot = PluginCommand<{ snapshot: Camera.Snapshot }>({ 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 a36603a5e0147c6b8f8fcb13545d0ea8217feee4..83e603b57e411f87a5ac59f4a8d3115cb1453e1a 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -28,7 +28,8 @@ export class PluginContext { readonly events = { state: { data: this.state.data.events, - behavior: this.state.behavior.events + behavior: this.state.behavior.events, + cameraSnapshots: this.state.cameraSnapshots.events } }; diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index b96729fd84a014043487bb65bf3c9b002e0d9fb1..ecff2513dfceaf0b63af797e681abd25ff33bee9 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -8,17 +8,20 @@ import { State } from 'mol-state'; import { PluginStateObject as SO } from './state/objects'; import { Camera } from 'mol-canvas3d/camera'; import { PluginBehavior } from './behavior'; +import { CameraSnapshotManager } from './state/camera'; export { PluginState } class PluginState { readonly data: State; readonly behavior: State; + readonly cameraSnapshots: CameraSnapshotManager = new CameraSnapshotManager(); getSnapshot(): PluginState.Snapshot { return { data: this.data.getSnapshot(), behaviour: this.behavior.getSnapshot(), + cameraSnapshots: this.cameraSnapshots.getStateSnapshot(), canvas3d: { camera: this.plugin.canvas3d.camera.getSnapshot() } @@ -28,12 +31,15 @@ class PluginState { async setSnapshot(snapshot: PluginState.Snapshot) { await this.plugin.runTask(this.behavior.setSnapshot(snapshot.behaviour)); await this.plugin.runTask(this.data.setSnapshot(snapshot.data)); + this.cameraSnapshots.setStateSnapshot(snapshot.cameraSnapshots); this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera); this.plugin.canvas3d.requestDraw(true); } dispose() { this.data.dispose(); + this.behavior.dispose(); + this.cameraSnapshots.dispose(); } constructor(private plugin: import('./context').PluginContext) { @@ -46,6 +52,7 @@ namespace PluginState { export interface Snapshot { data: State.Snapshot, behaviour: State.Snapshot, + cameraSnapshots: CameraSnapshotManager.StateSnapshot, canvas3d: { camera: Camera.Snapshot } diff --git a/src/mol-plugin/state/camera.ts b/src/mol-plugin/state/camera.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7874dbd7b7355d414c34e5cb42eb64b7742720b3 100644 --- a/src/mol-plugin/state/camera.ts +++ b/src/mol-plugin/state/camera.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Camera } from 'mol-canvas3d/camera'; +import { OrderedMap } from 'immutable'; +import { UUID } from 'mol-util'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; + +export { CameraSnapshotManager } + +class CameraSnapshotManager { + private ev = RxEventHelper.create(); + private _entries = OrderedMap<string, CameraSnapshotManager.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: CameraSnapshotManager.Entry) { + this._entries.set(e.id, e); + this.events.changed.next(); + } + + clear() { + if (this._entries.size === 0) return; + this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); + this.events.changed.next(); + } + + getStateSnapshot(): CameraSnapshotManager.StateSnapshot { + const entries: CameraSnapshotManager.Entry[] = []; + this._entries.forEach(e => entries.push(e!)); + return { entries }; + } + + setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) { + this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); + for (const e of state.entries) { + this._entries.set(e.id, e); + } + this.events.changed.next(); + } + + dispose() { + this.ev.dispose(); + } +} + +namespace CameraSnapshotManager { + export interface Entry { + id: UUID, + name: string, + description?: string, + snapshot: Camera.Snapshot + } + + export function Entry(name: string, snapshot: Camera.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/controls.tsx b/src/mol-plugin/ui/controls.tsx index ff1789b4b43cc5b356a18371e6d97615e8ed1a7c..39d97afe1ed6563964d8262a7883093865eaf97e 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -163,4 +163,66 @@ 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 76b81679317cefd411e482903fb254319e6dbe41..0d81076604e48056636b5d16e2c6490561f665b8 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -8,7 +8,7 @@ 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 } from './controls'; +import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls, CameraSnapshots } from './controls'; import { PluginComponent, PluginReactContext } from './base'; import { merge } from 'rxjs'; import { State } from 'mol-state'; @@ -33,6 +33,8 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { <_test_CurrentObject /> <hr /> <Controls /> + <hr /> + <CameraSnapshots /> </div> </div> </PluginReactContext.Provider>;