diff --git a/package.json b/package.json index 58063c0f529d6e7e66126062b60c766bf529dfca..0a94d9001e0050e427120077d692d1b8dab0eb35 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "watch": "concurrently --kill-others \"npm:watch-ts\" \"npm:watch-extra\" \"npm:watch-webpack\"", "watch-ts": "tsc -watch", "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/src/ --watch", - "build-webpack": "webpack --mode development", + "build-webpack": "webpack --mode production", "watch-webpack": "webpack -w --mode development", "model-server": "node build/src/servers/model/server.js", "model-server-watch": "nodemon --watch build/src build/src/servers/model/server.js" diff --git a/src/apps/basic-wrapper/index.html b/src/apps/basic-wrapper/index.html index b4a202a2dddd64da6f63e70dab00eb33de5e9744..07592b99d310d9376a10be671fa594b4314e01e0 100644 --- a/src/apps/basic-wrapper/index.html +++ b/src/apps/basic-wrapper/index.html @@ -12,37 +12,94 @@ } #app { position: absolute; - left: 100px; + left: 160px; top: 100px; width: 600px; height: 600px; border: 1px solid #ccc; } + + #controls { + position: absolute; + width: 130px; + top: 10px; + left: 10px; + } + + #controls > button { + display: block; + width: 100%; + text-align: left; + } + + #controls > hr { + margin: 5px 0; + } </style> <link rel="stylesheet" type="text/css" href="app.css" /> <script type="text/javascript" src="./index.js"></script> </head> <body> - <button id='spin'>Toggle Spin</button> - <button id='asym'>Load Asym Unit</button> - <button id='asm'>Load Assemly 1</button> + <div id='controls'> + + </div> <div id="app"></div> <script> - var pdbId = '5ire', assemblyId= '1'; + var pdbId = '1grm', assemblyId= '1'; var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif'; var format = 'cif'; // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent'; // var format = 'pdb'; + // var assemblyId = 'deposited'; BasicMolStarWrapper.init('app' /** or document.getElementById('app') */); BasicMolStarWrapper.setBackground(0xffffff); BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }); BasicMolStarWrapper.toggleSpin(); - document.getElementById('spin').onclick = () => BasicMolStarWrapper.toggleSpin(); - document.getElementById('asym').onclick = () => BasicMolStarWrapper.load({ url: url, format: format }); - document.getElementById('asm').onclick = () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }); + addHeader('Source'); + addControl('Load Asym Unit', () => BasicMolStarWrapper.load({ url: url, format: format })); + addControl('Load Assembly 1', () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId })); + + addSeparator(); + + addHeader('Camera'); + addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin()); + + addSeparator(); + + addHeader('Animation'); + + // adjust this number to make the animation faster or slower + // requires to "restart" the animation if changed + BasicMolStarWrapper.animate.modelIndex.maxFPS = 4; + + addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward()); + addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward()); + addControl('Play Palindrome', () => BasicMolStarWrapper.animate.modelIndex.palindrome()); + addControl('Play Loop', () => BasicMolStarWrapper.animate.modelIndex.loop()); + addControl('Stop', () => BasicMolStarWrapper.animate.modelIndex.stop()); + + //////////////////////////////////////////////////////// + + function addControl(label, action) { + var btn = document.createElement('button'); + btn.onclick = action; + btn.innerText = label; + document.getElementById('controls').appendChild(btn); + } + + function addSeparator() { + var hr = document.createElement('hr'); + document.getElementById('controls').appendChild(hr); + } + + function addHeader(header) { + var h = document.createElement('h3'); + h.innerText = header; + document.getElementById('controls').appendChild(h); + } </script> </body> </html> \ No newline at end of file diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts index b45753f912eb4053dc8cf6730f085a2bdb635042..8635c265cfd7892a9d851b4ba3d9928724cac056 100644 --- a/src/apps/basic-wrapper/index.ts +++ b/src/apps/basic-wrapper/index.ts @@ -13,6 +13,7 @@ import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/re import { Color } from 'mol-util/color'; import { StateTreeBuilder } from 'mol-state/tree/builder'; import { PluginStateObject as PSO } from 'mol-plugin/state/objects'; +import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; require('mol-plugin/skin/light.scss') type SupportedFormats = 'cif' | 'pdb' @@ -98,6 +99,17 @@ class BasicWrapper { PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } }); if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { }); } + + animate = { + modelIndex: { + maxFPS: 8, + onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) }, + onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) }, + palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) }, + loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) }, + stop: () => this.plugin.state.animation.stop() + } + } } (window as any).BasicMolStarWrapper = new BasicWrapper(); \ No newline at end of file diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 156219dead421927c46c3de787dc0a2740423850..5169f3f294581527fd08c255efce39de6923e58b 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -192,7 +192,7 @@ namespace Canvas3D { if (isIdentifying || isUpdating) return false let didRender = false - controls.update() + controls.update(currentTime); // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane. if (!camera.transition.inTransition) setClipping(); const cameraChanged = camera.updateMatrices(); @@ -230,6 +230,7 @@ namespace Canvas3D { } let forceNextDraw = false; + let currentTime = 0; function draw(force?: boolean) { if (render('draw', !!force || forceNextDraw)) { @@ -246,8 +247,8 @@ namespace Canvas3D { } function animate() { - const t = now(); - camera.transition.tick(t); + currentTime = now(); + camera.transition.tick(currentTime); draw(false) window.requestAnimationFrame(animate) } diff --git a/src/mol-canvas3d/controls/trackball.ts b/src/mol-canvas3d/controls/trackball.ts index 63234d324de095b95e09cd11ad07bd13354d4d95..b2e01c66d3206c082d0cbdd20b0da58f10eaf874 100644 --- a/src/mol-canvas3d/controls/trackball.ts +++ b/src/mol-canvas3d/controls/trackball.ts @@ -39,12 +39,12 @@ interface TrackballControls { readonly props: Readonly<TrackballControlsProps> setProps: (props: Partial<TrackballControlsProps>) => void - update: () => void + update: (t: number) => void reset: () => void dispose: () => void } namespace TrackballControls { - export function create (input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls { + export function create(input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls { const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props } const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 } @@ -131,7 +131,7 @@ namespace TrackballControls { Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye)) angle *= p.rotateSpeed; - Quat.setAxisAngle(rotQuat, rotAxis, angle ) + Quat.setAxisAngle(rotQuat, rotAxis, angle) Vec3.transformQuat(_eye, _eye, rotQuat) Vec3.transformQuat(object.up, object.up, rotQuat) @@ -150,7 +150,7 @@ namespace TrackballControls { Vec2.copy(_movePrev, _moveCurr) } - function zoomCamera () { + function zoomCamera() { const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed if (factor !== 1.0 && factor > 0.0) { Vec3.scale(_eye, _eye, factor) @@ -207,9 +207,11 @@ namespace TrackballControls { } } + let lastUpdated = -1; /** Update the object's position, direction and up vectors */ - function update() { - if (p.spin) spin(); + function update(t: number) { + if (lastUpdated === t) return; + if (p.spin) spin(t - lastUpdated); Vec3.sub(_eye, object.position, target) @@ -226,6 +228,8 @@ namespace TrackballControls { if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) { Vec3.copy(lastPosition, object.position) } + + lastUpdated = t; } /** Reset object's vectors and the target vector to their initial values */ @@ -299,15 +303,14 @@ namespace TrackballControls { } const _spinSpeed = Vec2.create(0.005, 0); - function spin() { - _spinSpeed[0] = (p.spinSpeed || 0) / 1000; + function spin(deltaT: number) { + const frameSpeed = (p.spinSpeed || 0) / 1000; + _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed; if (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed); } // force an update at start - update(); - - if (props.spin) { spin(); } + update(0); return { viewport, diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 2e53ede8c6ae73956b50ce2fc5061749bfd8a3a1..74424b9d615f8e31c7a024c893258a09e3be287c 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -51,7 +51,7 @@ export function SetCurrentObject(ctx: PluginContext) { } export function Update(ctx: PluginContext) { - PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.updateTree(tree))); + PluginCommands.State.Update.subscribe(ctx, ({ state, tree, doNotLogTiming }) => ctx.runTask(state.updateTree(tree, doNotLogTiming))); } export function ApplyAction(ctx: PluginContext) { diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index d17b98dae56c01efee9f206175a789062c7e10a2..5e99045b27de4231d3ba30f5d2314216221d64b9 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -17,7 +17,7 @@ export const PluginCommands = { State: { SetCurrentObject: PluginCommand<{ state: State, ref: Transform.Ref }>(), ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>(), - Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder }>(), + Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder, doNotLogTiming?: boolean }>(), RemoveObject: PluginCommand<{ state: State, ref: Transform.Ref }>(), diff --git a/src/mol-plugin/component.ts b/src/mol-plugin/component.ts index fab609d81a056aa2604fc2252da0f604c34fe72e..b49f80c1bb2d219fe334bbbf9b093bf37e66fa1e 100644 --- a/src/mol-plugin/component.ts +++ b/src/mol-plugin/component.ts @@ -4,41 +4,37 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { PluginContext } from './context'; import { shallowMergeArray } from 'mol-util/object'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; export class PluginComponent<State> { - private _state: BehaviorSubject<State>; - private _updated = new Subject(); + private _ev: RxEventHelper; - updateState(...states: Partial<State>[]): boolean { - const latest = this.latestState; + protected get ev() { + return this._ev || (this._ev = RxEventHelper.create()); + } + + private _state: State; + + protected updateState(...states: Partial<State>[]): boolean { + const latest = this.state; const s = shallowMergeArray(latest, states); if (s !== latest) { - this._state.next(s); + this._state = s; return true; } return false; } - get states() { - return <Observable<State>>this._state; - } - - get latestState() { - return this._state.value; - } - - get updated() { - return <Observable<{}>>this._updated; + get state() { + return this._state; } - triggerUpdate() { - this._updated.next({}); + dispose() { + if (this._ev) this._ev.dispose(); } - constructor(public context: PluginContext, initialState: State) { - this._state = new BehaviorSubject<State>(initialState); + constructor(initialState: State) { + this._state = initialState; } } \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 9125dc965485a80310fa522cb3526d059ef28ad2..ceace631931c4377eaea83565f5b8d95ac96d8db 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -98,7 +98,7 @@ export class PluginContext { initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) { try { this.layout.setRoot(container); - if (this.spec.initialLayout) this.layout.updateState(this.spec.initialLayout); + if (this.spec.initialLayout) this.layout.setProps(this.spec.initialLayout); (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container); PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } }); this.canvas3d.animate(); @@ -140,6 +140,7 @@ export class PluginContext { this.ev.dispose(); this.state.dispose(); this.tasks.dispose(); + this.layout.dispose(); this.disposed = true; } diff --git a/src/mol-plugin/layout.ts b/src/mol-plugin/layout.ts index bca2e2cd1da0b3570afb58a0b9f620c2e7530ca4..63b8f19be005def858b246ed5d7e31317289b9e4 100644 --- a/src/mol-plugin/layout.ts +++ b/src/mol-plugin/layout.ts @@ -42,21 +42,29 @@ interface RootState { } export class PluginLayout extends PluginComponent<PluginLayoutStateProps> { + readonly events = { + updated: this.ev() + } + private updateProps(state: Partial<PluginLayoutStateProps>) { - let prevExpanded = !!this.latestState.isExpanded; + let prevExpanded = !!this.state.isExpanded; this.updateState(state); if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand(); - this.triggerUpdate(); + this.events.updated.next(); } private root: HTMLElement; private rootState: RootState | undefined = void 0; private expandedViewport: HTMLMetaElement; + setProps(props: PluginLayoutStateProps) { + this.updateState(props); + } + setRoot(root: HTMLElement) { this.root = root; - if (this.latestState.isExpanded) this.handleExpand(); + if (this.state.isExpanded) this.handleExpand(); } private getScrollElement() { @@ -72,7 +80,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> { if (!body || !head) return; - if (this.latestState.isExpanded) { + if (this.state.isExpanded) { let children = head.children; let hasExp = false; let viewports: HTMLElement[] = []; @@ -167,8 +175,8 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> { } } - constructor(context: PluginContext) { - super(context, { ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout }); + constructor(private context: PluginContext) { + super({ ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout }); PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state)); diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index 5e3a349c88082113ea666c92133b15ed44591c7d..86d08ec73f24dcea3fbc75af8dffdfbd1a01dfcc 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -23,7 +23,6 @@ class PluginState { readonly behaviorState: State; readonly animation: PluginAnimationManager; readonly cameraSnapshots = new CameraSnapshotManager(); - readonly snapshots = new PluginStateSnapshotManager(); readonly behavior = { @@ -73,6 +72,7 @@ class PluginState { this.dataState.dispose(); this.behaviorState.dispose(); this.cameraSnapshots.dispose(); + this.animation.dispose(); } constructor(private plugin: import('./context').PluginContext) { diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts index cc6ef04c6a3a4e4eb089e625cce42e65e5fcd807..d8f3b054feb017bec2c805531812832e41fe8054 100644 --- a/src/mol-plugin/state/animation/built-in.ts +++ b/src/mol-plugin/state/animation/built-in.ts @@ -9,13 +9,20 @@ import { PluginStateObject } from '../objects'; import { StateTransforms } from '../transforms'; import { StateSelection } from 'mol-state/state/selection'; import { PluginCommands } from 'mol-plugin/command'; -import { ParamDefinition } from 'mol-util/param-definition'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; export const AnimateModelIndex = PluginStateAnimation.create({ name: 'built-in.animate-model-index', display: { name: 'Animate Model Index' }, - params: () => ({ maxFPS: ParamDefinition.Numeric(3, { min: 0.5, max: 30, step: 0.5 }) }), - initialState: () => ({ frame: 1 }), + params: () => ({ + mode: PD.MappedStatic('once', { + once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }, { isFlat: true }), + palindrome: PD.Group({ }), + loop: PD.Group({ }), + }, { options: [['once', 'Once'], ['palindrome', 'Palindrome'], ['loop', 'Loop']] }), + maxFPS: PD.Numeric(3, { min: 0.5, max: 30, step: 0.5 }) + }), + initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }), async apply(animState, t, ctx) { // limit fps if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) { @@ -28,19 +35,45 @@ export const AnimateModelIndex = PluginStateAnimation.create({ const update = state.build(); + const params = ctx.params; + const palindromeDirections = animState.palindromeDirections || { }; + let isEnd = false; + 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 = animState.frame % traj.data.length; - if (modelIndex < 0) modelIndex += traj.data.length; + const len = traj.data.length; + let dir: -1 | 1 = 1; + if (params.mode.name === 'once') { + dir = params.mode.params.direction === 'backward' ? -1 : 1; + // if we are at start or end already, do nothing. + if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) { + isEnd = true; + return old; + } + } else if (params.mode.name === 'palindrome') { + if (old.modelIndex === 0) dir = 1; + else if (old.modelIndex === len - 1) dir = -1; + else dir = palindromeDirections[m.transform.ref] || 1; + } + palindromeDirections[m.transform.ref] = dir; + + let modelIndex = (old.modelIndex + dir) % len; + if (modelIndex < 0) modelIndex += len; + + isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1); + return { modelIndex }; }); } - await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update }); - return { kind: 'next', state: { frame: animState.frame + 1 } }; + await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, doNotLogTiming: true }); + + if (params.mode.name === 'once' && isEnd) return { kind: 'finished' }; + if (params.mode.name === 'palindrome') return { kind: 'next', state: { palindromeDirections } }; + return { kind: 'next', state: {} }; } }) \ No newline at end of file diff --git a/src/mol-plugin/state/animation/manager.ts b/src/mol-plugin/state/animation/manager.ts index 8ba64a74932f7372782f4977b506c1f1b671aa5e..fc83a7f448e6177c3c4582b7910afbbf901be3c0 100644 --- a/src/mol-plugin/state/animation/manager.ts +++ b/src/mol-plugin/state/animation/manager.ts @@ -13,6 +13,7 @@ export { PluginAnimationManager } // TODO: pause functionality (this needs to reset if the state tree changes) // TODO: handle unregistered animations on state restore +// TODO: better API class PluginAnimationManager extends PluginComponent<PluginAnimationManager.State> { private map = new Map<string, PluginStateAnimation>(); @@ -20,9 +21,17 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat private _current: PluginAnimationManager.Current; private _params?: PD.For<PluginAnimationManager.State['params']> = void 0; + readonly events = { + updated: this.ev() + }; + get isEmpty() { return this.animations.length === 0; } get current() { return this._current!; } + private triggerUpdate() { + this.events.updated.next(); + } + getParams(): PD.Params { if (!this._params) { this._params = { @@ -35,9 +44,9 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat } updateParams(newParams: Partial<PluginAnimationManager.State['params']>) { - this.updateState({ params: { ...this.latestState.params, ...newParams } }); - const anim = this.map.get(this.latestState.params.current)!; - const params = anim.params(this.context); + this.updateState({ params: { ...this.state.params, ...newParams } }); + const anim = this.map.get(this.state.params.current)!; + const params = anim.params(this.context) as PD.Params; this._current = { anim, params, @@ -69,6 +78,16 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat } } + play<P>(animation: PluginStateAnimation<P>, params: P) { + this.stop(); + if (!this.map.has(animation.name)) { + this.register(animation); + } + this.updateParams({ current: animation.name }); + this.updateCurrentParams(params); + this.start(); + } + start() { this.updateState({ animationState: 'playing' }); this.triggerUpdate(); @@ -81,11 +100,19 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat } stop() { + if (typeof this._frame !== 'undefined') cancelAnimationFrame(this._frame); this.updateState({ animationState: 'stopped' }); this.triggerUpdate(); } + get isAnimating() { + return this.state.animationState === 'playing'; + } + + private _frame: number | undefined = void 0; private animate = async (t: number) => { + this._frame = void 0; + if (this._current.startedTime < 0) this._current.startedTime = t; const newState = await this._current.anim.apply( this._current.state, @@ -97,17 +124,17 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat } else if (newState.kind === 'next') { this._current.state = newState.state; this._current.lastTime = t - this._current.startedTime; - if (this.latestState.animationState === 'playing') requestAnimationFrame(this.animate); + if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate); } else if (newState.kind === 'skip') { - if (this.latestState.animationState === 'playing') requestAnimationFrame(this.animate); + if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate); } } getSnapshot(): PluginAnimationManager.Snapshot { - if (!this.current) return { state: this.latestState }; + if (!this.current) return { state: this.state }; return { - state: this.latestState, + state: this.state, current: { paramValues: this._current.paramValues, state: this._current.anim.stateSerialization ? this._current.anim.stateSerialization.toJSON(this._current.state) : this._current.state @@ -125,7 +152,7 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat ? this._current.anim.stateSerialization.fromJSON(snapshot.current.state) : snapshot.current.state; this.triggerUpdate(); - if (this.latestState.animationState === 'playing') this.resume(); + if (this.state.animationState === 'playing') this.resume(); } } @@ -135,8 +162,8 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat requestAnimationFrame(this.animate); } - constructor(ctx: PluginContext) { - super(ctx, { params: { current: '' }, animationState: 'stopped' }); + constructor(private context: PluginContext) { + super({ params: { current: '' }, animationState: 'stopped' }); } } diff --git a/src/mol-plugin/state/animation/model.ts b/src/mol-plugin/state/animation/model.ts index 82ba43ca9f3fccb271e97d59388221ea2f02d62b..88d99c653879f566900b18e04a639533d8e7c378 100644 --- a/src/mol-plugin/state/animation/model.ts +++ b/src/mol-plugin/state/animation/model.ts @@ -12,11 +12,11 @@ export { PluginStateAnimation } // TODO: helpers for building animations (once more animations are added) // for example "composite animation" -interface PluginStateAnimation<P extends PD.Params = any, S = any> { +interface PluginStateAnimation<P = any, S = any> { name: string, readonly display: { readonly name: string, readonly description?: string }, - params: (ctx: PluginContext) => P, - initialState(params: PD.Values<P>, ctx: PluginContext): S, + params: (ctx: PluginContext) => PD.For<P>, + initialState(params: P, ctx: PluginContext): S, /** * Apply the current frame and modify the state. @@ -38,12 +38,12 @@ namespace PluginStateAnimation { } export type ApplyResult<S> = { kind: 'finished' } | { kind: 'skip' } | { kind: 'next', state: S } - export interface Context<P extends PD.Params> { - params: PD.Values<P>, + export interface Context<P> { + params: P, plugin: PluginContext } - export function create<P extends PD.Params, S>(params: PluginStateAnimation<P, S>) { + export function create<P, S>(params: PluginStateAnimation<P, S>) { return params; } } \ No newline at end of file diff --git a/src/mol-plugin/state/camera.ts b/src/mol-plugin/state/camera.ts index b05a2b0692879bd8a0636b44f4ab53d362e2eae3..830dbaf8de461f343944f16c1d1e1cfc21021579 100644 --- a/src/mol-plugin/state/camera.ts +++ b/src/mol-plugin/state/camera.ts @@ -7,57 +7,53 @@ import { Camera } from 'mol-canvas3d/camera'; import { OrderedMap } from 'immutable'; import { UUID } from 'mol-util'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; +import { PluginComponent } from 'mol-plugin/component'; export { CameraSnapshotManager } -class CameraSnapshotManager { - private ev = RxEventHelper.create(); - private _entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); - +class CameraSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, CameraSnapshotManager.Entry> }> { readonly events = { changed: this.ev() }; - get entries() { return this._entries; } - getEntry(id: string) { - return this._entries.get(id); + return this.state.entries.get(id); } remove(id: string) { - if (!this._entries.has(id)) return; - this._entries.delete(id); + if (!this.state.entries.has(id)) return; + this.updateState({ entries: this.state.entries.delete(id) }); this.events.changed.next(); } add(e: CameraSnapshotManager.Entry) { - this._entries.set(e.id, e); + this.updateState({ entries: this.state.entries.set(e.id, e) }); this.events.changed.next(); } clear() { - if (this._entries.size === 0) return; - this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); + if (this.state.entries.size === 0) return; + this.updateState({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() }); this.events.changed.next(); } getStateSnapshot(): CameraSnapshotManager.StateSnapshot { const entries: CameraSnapshotManager.Entry[] = []; - this._entries.forEach(e => entries.push(e!)); + this.state.entries.forEach(e => entries.push(e!)); return { entries }; } setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) { - this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); + const entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); for (const e of state.entries) { - this._entries.set(e.id, e); + entries.set(e.id, e); } + this.updateState({ entries: entries.asImmutable() }); this.events.changed.next(); } - dispose() { - this.ev.dispose(); + constructor() { + super({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() }); } } diff --git a/src/mol-plugin/state/snapshots.ts b/src/mol-plugin/state/snapshots.ts index 8d03ed5ae64a12902d462b635d2826350e5eae55..d9f4152d2f8ebd6a1e04916a0a498f6a0ee2d91f 100644 --- a/src/mol-plugin/state/snapshots.ts +++ b/src/mol-plugin/state/snapshots.ts @@ -6,44 +6,39 @@ import { OrderedMap } from 'immutable'; import { UUID } from 'mol-util'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; import { PluginState } from '../state'; +import { PluginComponent } from 'mol-plugin/component'; export { PluginStateSnapshotManager } -class PluginStateSnapshotManager { - private ev = RxEventHelper.create(); - private _entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable(); - +class PluginStateSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, PluginStateSnapshotManager.Entry> }> { readonly events = { changed: this.ev() }; - get entries() { return this._entries; } - getEntry(id: string) { - return this._entries.get(id); + return this.state.entries.get(id); } remove(id: string) { - if (!this._entries.has(id)) return; - this._entries.delete(id); + if (!this.state.entries.has(id)) return; + this.updateState({ entries: this.state.entries.delete(id) }); this.events.changed.next(); } add(e: PluginStateSnapshotManager.Entry) { - this._entries.set(e.id, e); + this.updateState({ entries: this.state.entries.set(e.id, e) }); this.events.changed.next(); } clear() { - if (this._entries.size === 0) return; - this._entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable(); + if (this.state.entries.size === 0) return; + this.updateState({ entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() }); this.events.changed.next(); } - dispose() { - this.ev.dispose(); + constructor() { + super({ entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() }); } } diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index a28e72ada1d3fc3a30cddb12a12b8d493ca15bfd..4f67fc62cd221ab95984d506987628e2509057b3 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -24,6 +24,7 @@ import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-se import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif'; import { parsePDB } from 'mol-io/reader/pdb/parser'; import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb'; +import { Assembly } from 'mol-model/structure/model/properties/symmetry'; export { TrajectoryFromMmCif } type TrajectoryFromMmCif = typeof TrajectoryFromMmCif @@ -143,12 +144,26 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ return Task.create('Build Assembly', async ctx => { const model = a.data; let id = params.id; - let asm = ModelSymmetry.findAssembly(model, id || ''); - if (!!id && id !== 'deposited' && !asm) throw new Error(`Assembly '${id}' not found`); + let asm: Assembly | undefined = void 0; + + // if no id is specified, use the 1st assembly. + if (!id && model.symmetry.assemblies.length !== 0) { + id = model.symmetry.assemblies[0].id; + } + + if (model.symmetry.assemblies.length === 0) { + if (id !== 'deposited') { + plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`); + } + } else { + asm = ModelSymmetry.findAssembly(model, id || ''); + if (!asm) { + plugin.log.warn(`Model '${a.label}' has no assembly called '${id}', returning deposited structure.`); + } + } const base = Structure.ofModel(model); - if ((id && !asm) || model.symmetry.assemblies.length === 0) { - if (!!id && id !== 'deposited') plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`); + if (!asm) { const label = { label: a.data.label, description: structureDesc(base) }; return new SO.Molecule.Structure(base, label); } diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx index db43db77315ab37271bdd5b436115d5176272961..d4ae0171d84882a5f0c9fb6e2152b4fbfd5e2d83 100644 --- a/src/mol-plugin/ui/base.tsx +++ b/src/mol-plugin/ui/base.tsx @@ -10,7 +10,7 @@ import { PluginContext } from '../context'; export const PluginReactContext = React.createContext(void 0 as any as PluginContext); -export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> { +export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> { static contextType = PluginReactContext; readonly plugin: PluginContext; @@ -35,7 +35,7 @@ export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Com } } -export abstract class PurePluginComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> { +export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> { static contextType = PluginReactContext; readonly plugin: PluginContext; diff --git a/src/mol-plugin/ui/camera.tsx b/src/mol-plugin/ui/camera.tsx index ddcf5995d5fff6c62de49ea2e04d0f150da79a69..796bc5d869d85681a4af84d8f6711ec2cc3033bc 100644 --- a/src/mol-plugin/ui/camera.tsx +++ b/src/mol-plugin/ui/camera.tsx @@ -6,11 +6,11 @@ import { PluginCommands } from 'mol-plugin/command'; import * as React from 'react'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; -export class CameraSnapshots extends PluginComponent<{ }, { }> { +export class CameraSnapshots extends PluginUIComponent<{ }, { }> { render() { return <div> <div className='msp-section-header'>Camera Snapshots</div> @@ -20,7 +20,7 @@ export class CameraSnapshots extends PluginComponent<{ }, { }> { } } -class CameraSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> { +class CameraSnapshotControls extends PluginUIComponent<{ }, { name: string, description: string }> { static Params = { name: PD.Text(), description: PD.Text() @@ -48,7 +48,7 @@ class CameraSnapshotControls extends PluginComponent<{ }, { name: string, descri } } -class CameraSnapshotList extends PluginComponent<{ }, { }> { +class CameraSnapshotList extends PluginUIComponent<{ }, { }> { componentDidMount() { this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate()); } @@ -65,7 +65,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> { render() { return <ul style={{ listStyle: 'none' }} className='msp-state-list'> - {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}> + {this.plugin.state.cameraSnapshots.state.entries.valueSeq().map(e =><li key={e!.id}> <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button> <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'> <span className='msp-icon msp-icon-remove' /> diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 2a29958116c3495a7dc844ea0dd2e10f4643761a..db69594a3c13abea794a75df957abdd9dad58e19 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -7,10 +7,10 @@ import * as React from 'react'; import { PluginCommands } from 'mol-plugin/command'; import { UpdateTrajectory } from 'mol-plugin/state/actions/basic'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager'; -export class Controls extends PluginComponent<{ }, { }> { +export class Controls extends PluginUIComponent<{ }, { }> { render() { return <> @@ -18,7 +18,7 @@ export class Controls extends PluginComponent<{ }, { }> { } } -export class TrajectoryControls extends PluginComponent { +export class TrajectoryControls extends PluginUIComponent { render() { return <div> <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { @@ -37,7 +37,7 @@ export class TrajectoryControls extends PluginComponent { } } -export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> { +export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> { state = { entries: [] } componentDidMount() { diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index ad7ef082d48cf230cbe745d3e4bed7a610d6b545..868c44f16e20e6ae927fe732b197831b73504546 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -29,8 +29,10 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent< render() { const params = this.props.params; const values = this.props.values; + const keys = Object.keys(params); + if (keys.length === 0) return null; return <div style={{ width: '100%' }}> - {Object.keys(params).map(key => { + {keys.map(key => { const param = params[key]; if (param.isHidden) return null; const Control = controlFor(param); @@ -434,6 +436,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, render() { const params = this.props.param.params; + + // Do not show if there are no params. + if (Object.keys(params).length === 0) return null; + const label = this.props.param.label || camelCaseToWords(this.props.name); const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />; diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index e465a67a623ddfb0cf563a02590ad1bcbbeb53c5..e0ee8590fa071f3d79fbd924b49c8bb4d3931282 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -9,7 +9,7 @@ import { PluginContext } from '../context'; import { StateTree } from './state-tree'; import { Viewport, ViewportControls } from './viewport'; import { Controls, TrajectoryControls, LociLabelControl } from './controls'; -import { PluginComponent, PluginReactContext } from './base'; +import { PluginUIComponent, PluginReactContext } from './base'; import { CameraSnapshots } from './camera'; import { StateSnapshots } from './state'; import { List } from 'immutable'; @@ -39,9 +39,9 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { } } -class Layout extends PluginComponent { +class Layout extends PluginUIComponent { componentDidMount() { - this.subscribe(this.plugin.layout.updated, () => this.forceUpdate()); + this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate()); } region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) { @@ -53,7 +53,7 @@ class Layout extends PluginComponent { } render() { - const layout = this.plugin.layout.latestState; + const layout = this.plugin.layout.state; return <div className='msp-plugin'> <div className={`msp-plugin-content ${layout.isExpanded ? 'msp-layout-expanded' : 'msp-layout-standard msp-layout-standard-outside'}`}> <div className={layout.showControls ? 'msp-layout-hide-top' : 'msp-layout-hide-top msp-layout-hide-right msp-layout-hide-bottom msp-layout-hide-left'}> @@ -73,7 +73,7 @@ class Layout extends PluginComponent { } } -export class ViewportWrapper extends PluginComponent { +export class ViewportWrapper extends PluginUIComponent { render() { return <> <Viewport /> @@ -91,7 +91,7 @@ export class ViewportWrapper extends PluginComponent { } } -export class State extends PluginComponent { +export class State extends PluginUIComponent { componentDidMount() { this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate()); } @@ -113,18 +113,18 @@ export class State extends PluginComponent { } } -export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { +export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> { private wrapper = React.createRef<HTMLDivElement>(); componentDidMount() { - this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries.takeLast(100).toList() })); + this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries })); } componentDidUpdate() { this.scrollToBottom(); } - state = { entries: this.plugin.log.entries.takeLast(100).toList() }; + state = { entries: this.plugin.log.entries }; private scrollToBottom() { const log = this.wrapper.current; @@ -132,19 +132,26 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { } render() { + // TODO: ability to show full log + // showing more entries dramatically slows animations. + const maxEntries = 10; + const xs = this.state.entries, l = xs.size; + const entries: JSX.Element[] = []; + for (let i = Math.max(0, l - maxEntries), o = 0; i < l; i++) { + const e = xs.get(i); + entries.push(<li key={o++}> + <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} /> + <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div> + <div className='msp-log-entry'>{e!.message}</div> + </li>); + } return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}> - <ul className='msp-list-unstyled'> - {this.state.entries.map((e, i) => <li key={i}> - <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} /> - <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div> - <div className='msp-log-entry'>{e!.message}</div> - </li>)} - </ul> + <ul className='msp-list-unstyled'>{entries}</ul> </div>; } } -export class CurrentObject extends PluginComponent { +export class CurrentObject extends PluginUIComponent { get current() { return this.plugin.state.behavior.currentObject.value; } diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 59a5fc8c260476607d79dfe8a2bac39fd5ff1596..d91238056a0714691a169533c87f10c86155aa65 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -8,16 +8,16 @@ import * as React from 'react'; import { PluginStateObject } from 'mol-plugin/state/objects'; import { State, StateObject } from 'mol-state' import { PluginCommands } from 'mol-plugin/command'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; -export class StateTree extends PluginComponent<{ state: State }> { +export class StateTree extends PluginUIComponent<{ state: State }> { render() { const n = this.props.state.tree.root.ref; return <StateTreeNode state={this.props.state} nodeRef={n} />; } } -class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCollapsed: boolean }> { +class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State }, { state: State, isCollapsed: boolean }> { is(e: State.ObjectEvent) { return e.ref === this.props.nodeRef && e.state === this.props.state; } @@ -77,7 +77,7 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { } } -class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCurrent: boolean, isCollapsed: boolean }> { +class StateTreeNodeLabel extends PluginUIComponent<{ nodeRef: string, state: State }, { state: State, isCurrent: boolean, isCollapsed: boolean }> { is(e: State.ObjectEvent) { return e.ref === this.props.nodeRef && e.state === this.props.state; } diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx index b9a16ad129c0e3dd4fc737170b9675fe757ba850..6e13eb42e85f0cd17e167e54713ea618840bee76 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/src/mol-plugin/ui/state.tsx @@ -6,14 +6,14 @@ import { PluginCommands } from 'mol-plugin/command'; import * as React from 'react'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { shallowEqual } from 'mol-util'; import { List } from 'immutable'; import { ParameterControls } from './controls/parameters'; import { ParamDefinition as PD} from 'mol-util/param-definition'; import { Subject } from 'rxjs'; -export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> { +export class StateSnapshots extends PluginUIComponent<{ }, { serverUrl: string }> { state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' } updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) }; @@ -31,7 +31,7 @@ export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> // TODO: this is not nice: device some custom event system. const UploadedEvent = new Subject(); -class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { +class StateSnapshotControls extends PluginUIComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false }; static Params = { @@ -93,7 +93,7 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC } } -class LocalStateSnapshotList extends PluginComponent<{ }, { }> { +class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> { componentDidMount() { this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); } @@ -110,7 +110,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { render() { return <ul style={{ listStyle: 'none' }} className='msp-state-list'> - {this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}> + {this.plugin.state.snapshots.state.entries.valueSeq().map(e =><li key={e!.id}> <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button> <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'> <span className='msp-icon msp-icon-remove' /> @@ -121,7 +121,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { } type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string } -class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { +class RemoteStateSnapshotList extends PluginUIComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { state = { entries: List<RemoteEntry>(), isFetching: false }; componentDidMount() { diff --git a/src/mol-plugin/ui/state/animation.tsx b/src/mol-plugin/ui/state/animation.tsx index c25fc4edaa18045c164b7eb0395a23912a000b8d..64ebcc6c01f8474456d893097042057ca951cc17 100644 --- a/src/mol-plugin/ui/state/animation.tsx +++ b/src/mol-plugin/ui/state/animation.tsx @@ -5,12 +5,12 @@ */ import * as React from 'react'; -import { PluginComponent } from '../base'; +import { PluginUIComponent } from '../base'; import { ParameterControls, ParamOnChange } from '../controls/parameters'; -export class AnimationControls extends PluginComponent<{ }> { +export class AnimationControls extends PluginUIComponent<{ }> { componentDidMount() { - this.subscribe(this.plugin.state.animation.updated, () => this.forceUpdate()); + this.subscribe(this.plugin.state.animation.events.updated, () => this.forceUpdate()); } updateParams: ParamOnChange = p => { @@ -23,7 +23,7 @@ export class AnimationControls extends PluginComponent<{ }> { startOrStop = () => { const anim = this.plugin.state.animation; - if (anim.latestState.animationState === 'playing') anim.stop(); + if (anim.state.animationState === 'playing') anim.stop(); else anim.start(); } @@ -31,17 +31,17 @@ export class AnimationControls extends PluginComponent<{ }> { const anim = this.plugin.state.animation; if (anim.isEmpty) return null; - const isDisabled = anim.latestState.animationState === 'playing'; + const isDisabled = anim.state.animationState === 'playing'; return <div className='msp-animation-section'> <div className='msp-section-header'>Animations</div> - <ParameterControls params={anim.getParams()} values={anim.latestState.params} onChange={this.updateParams} isDisabled={isDisabled} /> + <ParameterControls params={anim.getParams()} values={anim.state.params} onChange={this.updateParams} isDisabled={isDisabled} /> <ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} /> <div className='msp-btn-row-group'> <button className='msp-btn msp-btn-block msp-form-control' onClick={this.startOrStop}> - {anim.latestState.animationState === 'playing' ? 'Stop' : 'Start'} + {anim.state.animationState === 'playing' ? 'Stop' : 'Start'} </button> </div> </div> diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx index daf5f2568aeca146e4cbf638d614a67ddc2906cf..b6570cbafc1cff2fbbe78c3fc2b7a3b9003a5e90 100644 --- a/src/mol-plugin/ui/state/common.tsx +++ b/src/mol-plugin/ui/state/common.tsx @@ -6,7 +6,7 @@ import { State, Transform, Transformer } from 'mol-state'; import * as React from 'react'; -import { PurePluginComponent } from '../base'; +import { PurePluginUIComponent } from '../base'; import { ParameterControls, ParamOnChange } from '../controls/parameters'; import { StateAction } from 'mol-state/action'; import { PluginContext } from 'mol-plugin/context'; @@ -15,7 +15,7 @@ import { Subject } from 'rxjs'; export { StateTransformParameters, TransformContolBase }; -class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> { +class StateTransformParameters extends PurePluginUIComponent<StateTransformParameters.Props> { validate(params: any) { // TODO return void 0; @@ -97,7 +97,7 @@ namespace TransformContolBase { } } -abstract class TransformContolBase<P, S extends TransformContolBase.ControlState> extends PurePluginComponent<P, S> { +abstract class TransformContolBase<P, S extends TransformContolBase.ControlState> extends PurePluginUIComponent<P, S> { abstract applyAction(): Promise<void>; abstract getInfo(): StateTransformParameters.Props['info']; abstract getHeader(): Transformer.Definition['display']; diff --git a/src/mol-plugin/ui/task.tsx b/src/mol-plugin/ui/task.tsx index c45b783ea05824070df202b5e24712b37cf61c09..20b1c7f14a072a0276ef709d4893c9f0785e8f8f 100644 --- a/src/mol-plugin/ui/task.tsx +++ b/src/mol-plugin/ui/task.tsx @@ -5,13 +5,13 @@ */ import * as React from 'react'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { OrderedMap } from 'immutable'; import { TaskManager } from 'mol-plugin/util/task-manager'; import { filter } from 'rxjs/operators'; import { Progress } from 'mol-task'; -export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> { +export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> { componentDidMount() { this.subscribe(this.plugin.events.task.progress.pipe(filter(e => e.level !== 'none')), e => { this.setState({ tracked: this.state.tracked.set(e.id, e) }) @@ -30,7 +30,7 @@ export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: Orde } } -class ProgressEntry extends PluginComponent<{ event: TaskManager.ProgressEvent }> { +class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent }> { render() { const root = this.props.event.progress.root; const subtaskCount = countSubtasks(this.props.event.progress.root) - 1; diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index b23bf8977b3a8aa7e33481ba23ae61b17863d00b..ce917a04dbbaa269c33d1c9115bdec8f818f5baf 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { ButtonsType } from 'mol-util/input/input-observer'; import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { PluginCommands } from 'mol-plugin/command'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; @@ -20,7 +20,7 @@ interface ViewportState { noWebGl: boolean } -export class ViewportControls extends PluginComponent { +export class ViewportControls extends PluginUIComponent { state = { isSettingsExpanded: false } @@ -35,11 +35,11 @@ export class ViewportControls extends PluginComponent { } toggleControls = () => { - PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.latestState.showControls } }); + PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.state.showControls } }); } toggleExpanded = () => { - PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.latestState.isExpanded } }); + PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.state.isExpanded } }); } setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => { @@ -55,7 +55,7 @@ export class ViewportControls extends PluginComponent { this.forceUpdate(); }); - this.subscribe(this.plugin.layout.updated, () => { + this.subscribe(this.plugin.layout.events.updated, () => { this.forceUpdate(); }); } @@ -73,15 +73,15 @@ export class ViewportControls extends PluginComponent { // TODO: show some icons dimmed etc.. return <div className={'msp-viewport-controls'}> <div className='msp-viewport-controls-buttons'> - {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.latestState.showControls)} - {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.latestState.isExpanded)} + {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)} + {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)} {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)} {this.icon('reset-scene', this.resetCamera, 'Reset Camera')} </div> {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'> <ControlGroup header='Layout' initialExpanded={true}> - <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.latestState} onChange={this.setLayout} /> + <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} /> </ControlGroup> <ControlGroup header='Viewport' initialExpanded={true}> <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} /> @@ -91,7 +91,7 @@ export class ViewportControls extends PluginComponent { } } -export class Viewport extends PluginComponent<{ }, ViewportState> { +export class Viewport extends PluginUIComponent<{ }, ViewportState> { private container = React.createRef<HTMLDivElement>(); private canvas = React.createRef<HTMLCanvasElement>(); @@ -128,7 +128,7 @@ export class Viewport extends PluginComponent<{ }, ViewportState> { idHelper.select(x, y); }); - this.subscribe(this.plugin.layout.updated, () => { + this.subscribe(this.plugin.layout.events.updated, () => { setTimeout(this.handleResize, 50); }); } diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts index 74562d92b5c968166cd0f2346af1d2ef92c0ef55..0a54889f02385057cb7fdc581b5e7f39e48bfdeb 100644 --- a/src/mol-plugin/util/canvas3d-identify.ts +++ b/src/mol-plugin/util/canvas3d-identify.ts @@ -52,7 +52,7 @@ export class Canvas3dIdentifyHelper { } private animate: (t: number) => void = t => { - if (this.inside && t - this.prevT > 1000 / this.maxFps) { + if (!this.ctx.state.animation.isAnimating && this.inside && t - this.prevT > 1000 / this.maxFps) { this.prevT = t; this.currentIdentifyT = t; this.identify(false, t); diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index e8bdf73751d8d4b7fe63d5cffb7c6a834ba4204a..857d3249f5ea0cda44ad9f39f4d1cbf63e014d53 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -59,7 +59,6 @@ interface StateObjectCell<T = StateObject> { // Which object was used as a parent to create data in this cell sourceRef: Transform.Ref | undefined, - version: string status: StateObjectCell.Status, params: { diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 42030da32278c8e08da5aefae600c17bf1d455ea..766daa3fb470882e8c853923eb25567fa2f9ce6f 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -8,7 +8,6 @@ import { StateObject, StateObjectCell } from './object'; import { StateTree } from './tree'; import { Transform } from './transform'; import { Transformer } from './transformer'; -import { UUID } from 'mol-util'; import { RuntimeContext, Task } from 'mol-task'; import { StateSelection } from './state/selection'; import { RxEventHelper } from 'mol-util/rx-event-helper'; @@ -184,7 +183,6 @@ class State { sourceRef: void 0, obj: rootObject, status: 'ok', - version: root.version, errorText: void 0, params: { definition: {}, @@ -350,7 +348,7 @@ function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: State function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) { const cell = s.cells.get(n.ref); - if (!cell || cell.version !== n.version || cell.status === 'error') { + if (!cell || cell.transform.version !== n.version || cell.status === 'error') { s.roots.push(n.ref); return false; } @@ -406,7 +404,6 @@ function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCell transform, sourceRef: void 0, status: 'pending', - version: UUID.create22(), errorText: void 0, params: void 0 }; @@ -556,7 +553,6 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo // special case for Root if (current.transform.ref === Transform.RootRef) { - current.version = transform.version; return { action: 'none' }; } @@ -574,7 +570,6 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo current.params = params; const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values); current.obj = obj; - current.version = transform.version; return { ref: currentRef, action: 'created', obj }; } else { @@ -591,14 +586,11 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo const oldObj = current.obj; const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams); current.obj = newObj; - current.version = transform.version; return { ref: currentRef, action: 'replaced', oldObj, obj: newObj }; } case Transformer.UpdateResult.Updated: - current.version = transform.version; return { ref: currentRef, action: 'updated', obj: current.obj! }; default: - current.version = transform.version; return { action: 'none' }; } } diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts index 828f7c9cf85a0926dae10d13bc6c675094939b64..d9aec37f87f11cff889c322832dd020e0c3b16dc 100644 --- a/src/mol-state/state/selection.ts +++ b/src/mol-state/state/selection.ts @@ -29,7 +29,7 @@ namespace StateSelection { } function isObj(arg: any): arg is StateObjectCell { - return (arg as StateObjectCell).version !== void 0 && (arg as StateObjectCell).transform !== void 0; + return (arg as StateObjectCell).transform !== void 0 && (arg as StateObjectCell).status !== void 0; } function isBuilder(arg: any): arg is Builder { diff --git a/webpack.config.js b/webpack.config.js index a25ec62f361996fd8f2030ee13133d11d0227d44..7abfd52d3558fe5f8dab53a9dfddd5104d4d807e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,6 +49,8 @@ const sharedConfig = { }), new webpack.DefinePlugin({ __PLUGIN_VERSION_TIMESTAMP__: webpack.DefinePlugin.runtimeValue(() => `${new Date().valueOf()}`, true), + // include this for production version of React + // 'process.env.NODE_ENV': JSON.stringify('production') }), new MiniCssExtractPlugin({ filename: 'app.css' }) ],