diff --git a/src/apps/canvas/app.ts b/src/apps/canvas/app.ts index fc8e304edd25da52c0e3bbe3f34432e4f4b62356..2dd92e472497572264c5a63c230c2199aeb5766d 100644 --- a/src/apps/canvas/app.ts +++ b/src/apps/canvas/app.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import Canvas3D from 'mol-canvas3d/canvas3d'; +import { Canvas3D } from 'mol-canvas3d/canvas3d'; import { getCifFromUrl, getModelsFromMmcif, getCifFromFile, getCcp4FromUrl, getVolumeFromCcp4, getCcp4FromFile, getVolumeFromVolcif } from './util'; import { StructureView } from './structure-view'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/apps/canvas/component/representation.tsx b/src/apps/canvas/component/representation.tsx index 261f54e4518d054c22e66e2e2bb5fd211db3d9ed..7d395d5e9828882b9c26aa699339ef6a6acdac0d 100644 --- a/src/apps/canvas/component/representation.tsx +++ b/src/apps/canvas/component/representation.tsx @@ -5,7 +5,7 @@ */ import * as React from 'react' -import Canvas3D from 'mol-canvas3d/canvas3d'; +import { Canvas3D } from 'mol-canvas3d/canvas3d'; import { App } from '../app'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Representation } from 'mol-repr/representation'; diff --git a/src/apps/canvas/component/viewport.tsx b/src/apps/canvas/component/viewport.tsx index e0a5fdb780824b08e43b5a9947d97f335eda3fea..c6c6f547b6480d432581e53e215eb59351913690 100644 --- a/src/apps/canvas/component/viewport.tsx +++ b/src/apps/canvas/component/viewport.tsx @@ -11,7 +11,7 @@ import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci'; import { labelFirst } from 'mol-theme/label'; import { ButtonsType } from 'mol-util/input/input-observer'; import { throttleTime } from 'rxjs/operators' -import { CombinedCameraMode } from 'mol-canvas3d/camera/combined'; +import { Camera } from 'mol-canvas3d/camera'; import { ColorParamComponent } from 'mol-app/component/parameter/color'; import { Color } from 'mol-util/color'; import { ParamDefinition as PD } from 'mol-util/param-definition' @@ -24,7 +24,7 @@ interface ViewportState { noWebGl: boolean pickingInfo: string taskInfo: string - cameraMode: CombinedCameraMode + cameraMode: Camera.Mode backgroundColor: Color } @@ -148,7 +148,7 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> { value={this.state.cameraMode} style={{width: '150'}} onChange={e => { - const p = { cameraMode: e.target.value as CombinedCameraMode } + const p = { cameraMode: e.target.value as Camera.Mode } this.props.app.canvas3d.setProps(p) this.setState(p) }} diff --git a/src/apps/canvas/structure-view.ts b/src/apps/canvas/structure-view.ts index f82151cda14b7afb1a3b546b067f400d7f17f14b..33a4f9faab2fffc3a19cdf3d8193eb741afb0862 100644 --- a/src/apps/canvas/structure-view.ts +++ b/src/apps/canvas/structure-view.ts @@ -8,7 +8,7 @@ import { Model, Structure } from 'mol-model/structure'; import { getStructureFromModel } from './util'; import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry'; import { getAxesShape } from './assembly-symmetry'; -import Canvas3D from 'mol-canvas3d/canvas3d'; +import { Canvas3D } from 'mol-canvas3d/canvas3d'; // import { MeshBuilder } from 'mol-geo/mesh/mesh-builder'; // import { addSphere } from 'mol-geo/mesh/builder/sphere'; // import { Shape } from 'mol-model/shape'; @@ -213,7 +213,7 @@ export async function StructureView(app: App, canvas3d: Canvas3D, models: Readon } } - canvas3d.center(structure.boundary.sphere.center) + canvas3d.camera.setState({ target: structure.boundary.sphere.center }) // const mb = MeshBuilder.create() // mb.setGroup(0) diff --git a/src/apps/canvas/volume-view.ts b/src/apps/canvas/volume-view.ts index 023f16fec99ffc3be3bdee4b09139dac1f43e65c..850cba42831ac8cc7bb4a2d55d97a08cc9ae503f 100644 --- a/src/apps/canvas/volume-view.ts +++ b/src/apps/canvas/volume-view.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import Canvas3D from 'mol-canvas3d/canvas3d'; +import { Canvas3D } from 'mol-canvas3d/canvas3d'; import { BehaviorSubject } from 'rxjs'; import { App } from './app'; import { VolumeData } from 'mol-model/volume'; diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html index e65ee3281561d7e5cf111c03740d03ffa38072fb..9a57c1d04577e84284f8bb4b9fcf4318a918b97f 100644 --- a/src/apps/viewer/index.html +++ b/src/apps/viewer/index.html @@ -8,6 +8,7 @@ * { margin: 0; padding: 0; + box-sizing: border-box; } html, body { width: 100%; diff --git a/src/examples/task.ts b/src/examples/task.ts index 64143cfb7da152845fa702b6854eb0c5f677c018..0741906538cfd7bedea64365d73a15fa28fbca07 100644 --- a/src/examples/task.ts +++ b/src/examples/task.ts @@ -4,7 +4,8 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Task, Progress, Scheduler, now, MultistepTask, chunkedSubtask } from 'mol-task' +import { Task, Progress, Scheduler, MultistepTask, chunkedSubtask } from 'mol-task' +import { now } from 'mol-util/now'; export async function test1() { const t = Task.create('test', async () => 1); diff --git a/src/mol-app/component/parameters.tsx b/src/mol-app/component/parameters.tsx index 02e47939a7d0a7b064c8482d3ea8ab88917a6347..c5c0c673ccecee6122783cfe0e0ca61427dd4d9e 100644 --- a/src/mol-app/component/parameters.tsx +++ b/src/mol-app/component/parameters.tsx @@ -48,7 +48,7 @@ export class ParametersComponent<P extends PD.Params> extends React.Component<Pa } render() { - return <div> + return <div style={{ width: '100%' }}> { Object.keys(this.props.params).map(k => { const param = this.props.params[k] const value = this.props.values[k] diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2e482abc163884b35a6565ec0d160c8de177b67 --- /dev/null +++ b/src/mol-canvas3d/camera.ts @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Mat4, Vec3, Vec4, EPSILON } from 'mol-math/linear-algebra' +import { Viewport, cameraLookAt, cameraProject, cameraUnproject } from './camera/util'; +import { Object3D } from 'mol-gl/object3d'; +import { BehaviorSubject } from 'rxjs'; + +export { Camera } + +// TODO: slab controls that modify near/far planes? + +class Camera implements Object3D { + readonly updatedViewProjection = new BehaviorSubject<Camera>(this); + + readonly view: Mat4 = Mat4.identity(); + readonly projection: Mat4 = Mat4.identity(); + readonly projectionView: Mat4 = Mat4.identity(); + readonly inverseProjectionView: Mat4 = Mat4.identity(); + + readonly viewport: Viewport; + readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot(); + + get position() { return this.state.position; } + set position(v: Vec3) { Vec3.copy(this.state.position, v); } + + get direction() { return this.state.direction; } + set direction(v: Vec3) { Vec3.copy(this.state.direction, v); } + + get up() { return this.state.up; } + set up(v: Vec3) { Vec3.copy(this.state.up, v); } + + get target() { return this.state.target; } + set target(v: Vec3) { Vec3.copy(this.state.target, v); } + + private prevProjection = Mat4.identity(); + private prevView = Mat4.identity(); + + updateMatrices() { + const snapshot = this.state as Camera.Snapshot; + const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target); + snapshot.zoom = this.viewport.height / height; + + switch (this.state.mode) { + case 'orthographic': updateOrtho(this); break; + case 'perspective': updatePers(this); break; + default: throw new Error('unknown camera mode'); + } + + const changed = !Mat4.areEqual(this.projection, this.prevProjection, EPSILON.Value) || !Mat4.areEqual(this.view, this.prevView, EPSILON.Value); + + Mat4.mul(this.projectionView, this.projection, this.view) + Mat4.invert(this.inverseProjectionView, this.projectionView) + + + if (changed) { + Mat4.mul(this.projectionView, this.projection, this.view) + Mat4.invert(this.inverseProjectionView, this.projectionView) + + Mat4.copy(this.prevView, this.view); + Mat4.copy(this.prevProjection, this.projection); + this.updatedViewProjection.next(this); + } + + return changed; + } + + setState(snapshot?: Partial<Camera.Snapshot>) { + Camera.copySnapshot(this.state, snapshot); + } + + getSnapshot() { + const ret = Camera.createDefaultSnapshot(); + Camera.copySnapshot(ret, this.state); + return ret; + } + + lookAt(target: Vec3) { + cameraLookAt(this.direction, this.up, this.position, target); + } + + translate(v: Vec3) { + Vec3.add(this.position, this.position, v) + } + + project(out: Vec4, point: Vec3) { + return cameraProject(out, point, this.viewport, this.projectionView) + } + + unproject(out: Vec3, point: Vec3) { + return cameraUnproject(out, point, this.viewport, this.inverseProjectionView) + } + + dispose() { + this.updatedViewProjection.complete(); + } + + constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) { + this.viewport = viewport; + Camera.copySnapshot(this.state, state); + } + +} + +namespace Camera { + export type Mode = 'perspective' | 'orthographic' + + export function createDefaultSnapshot(): Snapshot { + return { + mode: 'perspective', + + position: Vec3.zero(), + direction: Vec3.create(0, 0, -1), + up: Vec3.create(0, 1, 0), + + target: Vec3.create(0, 0, 0), + + near: 0.1, + far: 10000, + fogNear: 0.1, + fogFar: 10000, + + fov: Math.PI / 4, + zoom: 1 + }; + } + + export interface Snapshot { + mode: Mode, + + position: Vec3, + direction: Vec3, + up: Vec3, + target: Vec3, + + near: number, + far: number, + fogNear: number, + fogFar: number, + + fov: number, + zoom: number + } + + export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) { + if (!source) return; + + if (typeof source.mode !== 'undefined') out.mode = source.mode; + + if (typeof source.position !== 'undefined') Vec3.copy(out.position, source.position); + if (typeof source.direction !== 'undefined') Vec3.copy(out.direction, source.direction); + if (typeof source.up !== 'undefined') Vec3.copy(out.up, source.up); + if (typeof source.target !== 'undefined') Vec3.copy(out.target, source.target); + + if (typeof source.near !== 'undefined') out.near = source.near; + if (typeof source.far !== 'undefined') out.far = source.far; + if (typeof source.fogNear !== 'undefined') out.fogNear = source.fogNear; + if (typeof source.fogFar !== 'undefined') out.fogFar = source.fogFar; + + if (typeof source.fov !== 'undefined') out.fov = source.fov; + if (typeof source.zoom !== 'undefined') out.zoom = source.zoom; + } +} + +const _center = Vec3.zero(); +function updateOrtho(camera: Camera) { + const { viewport, state: { zoom, near, far } } = camera; + + const fullLeft = (viewport.width - viewport.x) / -2 + const fullRight = (viewport.width - viewport.x) / 2 + const fullTop = (viewport.height - viewport.y) / 2 + const fullBottom = (viewport.height - viewport.y) / -2 + + const dx = (fullRight - fullLeft) / (2 * zoom) + const dy = (fullTop - fullBottom) / (2 * zoom) + const cx = (fullRight + fullLeft) / 2 + const cy = (fullTop + fullBottom) / 2 + + const left = cx - dx + const right = cx + dx + const top = cy + dy + const bottom = cy - dy + + // build projection matrix + Mat4.ortho(camera.projection, left, right, bottom, top, Math.abs(near), Math.abs(far)) + + // build view matrix + Vec3.add(_center, camera.position, camera.direction) + Mat4.lookAt(camera.view, camera.position, _center, camera.up) +} + +function updatePers(camera: Camera) { + const aspect = camera.viewport.width / camera.viewport.height + + const { fov, near, far } = camera.state; + + // build projection matrix + Mat4.perspective(camera.projection, fov, aspect, Math.abs(near), Math.abs(far)) + + // build view matrix + Vec3.add(_center, camera.position, camera.direction) + Mat4.lookAt(camera.view, camera.position, _center, camera.up) +} \ No newline at end of file diff --git a/src/mol-canvas3d/camera/base.ts b/src/mol-canvas3d/camera/base.ts deleted file mode 100644 index 3350d411fc58364279b9f644ae5a4cdf1be60092..0000000000000000000000000000000000000000 --- a/src/mol-canvas3d/camera/base.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Mat4, Vec3, Vec4 } from 'mol-math/linear-algebra' -import { cameraProject, cameraUnproject, cameraLookAt, Viewport } from './util'; -import { Object3D } from 'mol-gl/object3d'; - -export interface Camera extends Object3D { - readonly projection: Mat4, - readonly projectionView: Mat4, - readonly inverseProjectionView: Mat4, - readonly viewport: Viewport, - - near: number, - far: number, - fogNear: number, - fogFar: number, -} - -export const DefaultCameraProps = { - position: Vec3.zero(), - direction: Vec3.create(0, 0, -1), - up: Vec3.create(0, 1, 0), - viewport: Viewport.create(-1, -1, 1, 1), - target: Vec3.create(0, 0, 0), - - near: 0.1, - far: 10000, - fogNear: 0.1, - fogFar: 10000, -} -export type CameraProps = typeof DefaultCameraProps - -export namespace Camera { - export function create(props?: Partial<CameraProps>): Camera { - const p = { ...DefaultCameraProps, ...props }; - - const { view, position, direction, up } = Object3D.create() - Vec3.copy(position, p.position) - Vec3.copy(direction, p.direction) - Vec3.copy(up, p.up) - - const projection = Mat4.identity() - const viewport = Viewport.clone(p.viewport) - const projectionView = Mat4.identity() - const inverseProjectionView = Mat4.identity() - - return { - projection, - projectionView, - inverseProjectionView, - viewport, - - view, - position, - direction, - up, - - near: p.near, - far: p.far, - fogNear: p.fogNear, - fogFar: p.fogFar, - } - } - - export function update (camera: Camera) { - Mat4.mul(camera.projectionView, camera.projection, camera.view) - Mat4.invert(camera.inverseProjectionView, camera.projectionView) - return camera - } - - export function lookAt (camera: Camera, target: Vec3) { - cameraLookAt(camera.direction, camera.up, camera.position, target) - } - - export function reset (camera: Camera, props: CameraProps) { - Vec3.copy(camera.position, props.position) - Vec3.copy(camera.direction, props.direction) - Vec3.copy(camera.up, props.up) - Mat4.setIdentity(camera.view) - Mat4.setIdentity(camera.projection) - Mat4.setIdentity(camera.projectionView) - Mat4.setIdentity(camera.inverseProjectionView) - } - - export function translate (camera: Camera, v: Vec3) { - Vec3.add(camera.position, camera.position, v) - } - - export function project (camera: Camera, out: Vec4, point: Vec3) { - return cameraProject(out, point, camera.viewport, camera.projectionView) - } - - export function unproject (camera: Camera, out: Vec3, point: Vec3) { - return cameraUnproject(out, point, camera.viewport, camera.inverseProjectionView) - } -} \ No newline at end of file diff --git a/src/mol-canvas3d/camera/combined.ts b/src/mol-canvas3d/camera/combined.ts deleted file mode 100644 index e0b1ed41c934f312cf2e9373c1d1ccd72f701f68..0000000000000000000000000000000000000000 --- a/src/mol-canvas3d/camera/combined.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { PerspectiveCamera, } from './perspective'; -import { OrthographicCamera } from './orthographic'; -import { Camera, DefaultCameraProps } from './base'; -import { Vec3 } from 'mol-math/linear-algebra'; - -export type CombinedCameraMode = 'perspective' | 'orthographic' - -export interface CombinedCamera extends Camera { - target: Vec3 - fov: number - zoom: number - mode: CombinedCameraMode -} - -export const DefaultCombinedCameraProps = { - ...DefaultCameraProps, - target: Vec3.zero(), - fov: Math.PI / 4, - zoom: 1, - mode: 'perspective' as CombinedCameraMode -} -export type CombinedCameraProps = Partial<typeof DefaultCombinedCameraProps> - -export namespace CombinedCamera { - export function create(props: CombinedCameraProps = {}): CombinedCamera { - const { zoom, fov, mode, target: t } = { ...DefaultCombinedCameraProps, ...props }; - const target = Vec3.create(t[0], t[1], t[2]) - const camera = { ...Camera.create(props), zoom, fov, mode, target } - update(camera) - - return camera - } - - export function update(camera: CombinedCamera) { - const height = 2 * Math.tan(camera.fov / 2) * Vec3.distance(camera.position, camera.target) - camera.zoom = camera.viewport.height / height - - switch (camera.mode) { - case 'orthographic': OrthographicCamera.update(camera); break - case 'perspective': PerspectiveCamera.update(camera); break - } - } -} \ No newline at end of file diff --git a/src/mol-canvas3d/camera/orthographic.ts b/src/mol-canvas3d/camera/orthographic.ts deleted file mode 100644 index 26321535ff6021972f2cfda45c4004039aa3698b..0000000000000000000000000000000000000000 --- a/src/mol-canvas3d/camera/orthographic.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Mat4, Vec3 } from 'mol-math/linear-algebra' -import { DefaultCameraProps, Camera } from './base' - -export interface OrthographicCamera extends Camera { - zoom: number -} - -export const DefaultOrthographicCameraProps = { - ...DefaultCameraProps, - zoom: 1 -} -export type OrthographicCameraProps = Partial<typeof DefaultOrthographicCameraProps> - -export namespace OrthographicCamera { - export function create(props: OrthographicCameraProps = {}): OrthographicCamera { - const { zoom } = { ...DefaultOrthographicCameraProps, ...props }; - const camera = { ...Camera.create(props), zoom } - update(camera) - - return camera - } - - const center = Vec3.zero() - export function update(camera: OrthographicCamera) { - const { viewport, zoom } = camera - - const fullLeft = (viewport.width - viewport.x) / -2 - const fullRight = (viewport.width - viewport.x) / 2 - const fullTop = (viewport.height - viewport.y) / 2 - const fullBottom = (viewport.height - viewport.y) / -2 - - const dx = (fullRight - fullLeft) / (2 * zoom) - const dy = (fullTop - fullBottom) / (2 * zoom) - const cx = (fullRight + fullLeft) / 2 - const cy = (fullTop + fullBottom) / 2 - - const left = cx - dx - const right = cx + dx - const top = cy + dy - const bottom = cy - dy - - // build projection matrix - Mat4.ortho(camera.projection, left, right, bottom, top, Math.abs(camera.near), Math.abs(camera.far)) - - // build view matrix - Vec3.add(center, camera.position, camera.direction) - Mat4.lookAt(camera.view, camera.position, center, camera.up) - - // update projection * view and invert - Camera.update(camera) - } -} \ No newline at end of file diff --git a/src/mol-canvas3d/camera/perspective.ts b/src/mol-canvas3d/camera/perspective.ts deleted file mode 100644 index b00f427aa776900974d2aec42ce4902a3dca7911..0000000000000000000000000000000000000000 --- a/src/mol-canvas3d/camera/perspective.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Mat4, Vec3 } from 'mol-math/linear-algebra' -import { DefaultCameraProps, Camera } from './base' - -export interface PerspectiveCamera extends Camera { - fov: number -} - -export const DefaultPerspectiveCameraProps = { - ...DefaultCameraProps, - fov: Math.PI / 4, -} -export type PerspectiveCameraProps = Partial<typeof DefaultPerspectiveCameraProps> - -export namespace PerspectiveCamera { - export function create(props: PerspectiveCameraProps = {}): PerspectiveCamera { - const { fov } = { ...DefaultPerspectiveCameraProps, ...props } - const camera = { ...Camera.create(props), fov } - update(camera) - - return camera - } - - const center = Vec3.zero() - export function update(camera: PerspectiveCamera) { - const aspect = camera.viewport.width / camera.viewport.height - - // build projection matrix - Mat4.perspective(camera.projection, camera.fov, aspect, Math.abs(camera.near), Math.abs(camera.far)) - - // build view matrix - Vec3.add(center, camera.position, camera.direction) - Mat4.lookAt(camera.view, camera.position, center, camera.up) - - // update projection * view and invert - Camera.update(camera) - } -} \ No newline at end of file diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 7d60e100eb25a604ab1b9ab7370b47f5a5b36738..420ca3987ebabc1009d174f894bf765ad608fe46 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -5,8 +5,9 @@ */ import { BehaviorSubject, Subscription } from 'rxjs'; +import { now } from 'mol-util/now'; -import { Vec3, Mat4, EPSILON } from 'mol-math/linear-algebra' +import { Vec3 } from 'mol-math/linear-algebra' import InputObserver from 'mol-util/input/input-observer' import * as SetUtils from 'mol-util/set' import Renderer, { RendererStats } from 'mol-gl/renderer' @@ -24,20 +25,22 @@ import { PickingId, decodeIdRGB } from 'mol-geo/geometry/picking'; import { MarkerAction } from 'mol-geo/geometry/marker-data'; import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci'; import { Color } from 'mol-util/color'; -import { CombinedCamera, CombinedCameraMode } from './camera/combined'; +import { Camera } from './camera'; export const DefaultCanvas3DProps = { + // TODO: FPS cap? + // maxFps: 30, cameraPosition: Vec3.create(0, 0, 50), - cameraMode: 'perspective' as CombinedCameraMode, + cameraMode: 'perspective' as Camera.Mode, backgroundColor: Color(0x000000), } export type Canvas3DProps = typeof DefaultCanvas3DProps +export { Canvas3D } + interface Canvas3D { readonly webgl: WebGLContext, - center: (p: Vec3) => void - hide: (repr: Representation.Any) => void show: (repr: Representation.Any) => void @@ -46,7 +49,7 @@ interface Canvas3D { update: () => void clear: () => void - draw: (force?: boolean) => void + // draw: (force?: boolean) => void requestDraw: (force?: boolean) => void animate: () => void pick: () => void @@ -54,13 +57,11 @@ interface Canvas3D { mark: (loci: Loci, action: MarkerAction) => void getLoci: (pickingId: PickingId) => { loci: Loci, repr?: Representation.Any } - readonly reprCount: BehaviorSubject<number> - readonly identified: BehaviorSubject<string> - readonly didDraw: BehaviorSubject<number> + readonly didDraw: BehaviorSubject<now.Timestamp> handleResize: () => void resetCamera: () => void - readonly camera: CombinedCamera + readonly camera: Camera downloadScreenshot: () => void getImageData: (variant: RenderVariant) => ImageData setProps: (props: Partial<Canvas3DProps>) => void @@ -79,13 +80,12 @@ namespace Canvas3D { const reprRenderObjects = new Map<Representation.Any, Set<RenderObject>>() const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>() const reprCount = new BehaviorSubject(0) - const identified = new BehaviorSubject('') - const startTime = performance.now() - const didDraw = new BehaviorSubject(0) + const startTime = now() + const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp) const input = InputObserver.create(canvas) - const camera = CombinedCamera.create({ + const camera = new Camera({ near: 0.1, far: 10000, position: Vec3.clone(p.cameraPosition), @@ -118,8 +118,6 @@ namespace Canvas3D { let isPicking = false let drawPending = false let lastRenderTime = -1 - const prevProjectionView = Mat4.zero() - const prevSceneView = Mat4.zero() function getLoci(pickingId: PickingId) { let loci: Loci = EmptyLoci @@ -156,7 +154,7 @@ namespace Canvas3D { // return 0 // } - function render(variant: RenderVariant, force?: boolean) { + function render(variant: RenderVariant, force: boolean) { if (isPicking) return false // const p = scene.boundingSphere.center // console.log(p[0], p[1], p[2]) @@ -178,51 +176,56 @@ namespace Canvas3D { // console.log(camera.fogNear, camera.fogFar, targetDistance) - switch (variant) { - case 'pickObject': objectPickTarget.bind(); break; - case 'pickInstance': instancePickTarget.bind(); break; - case 'pickGroup': groupPickTarget.bind(); break; - case 'draw': - webgl.unbindFramebuffer(); - renderer.setViewport(0, 0, canvas.width, canvas.height); - break; - } let didRender = false controls.update() - CombinedCamera.update(camera) - if (force || !Mat4.areEqual(camera.projectionView, prevProjectionView, EPSILON.Value) || !Mat4.areEqual(scene.view, prevSceneView, EPSILON.Value)) { - // console.log('foo', force, prevSceneView, scene.view) - Mat4.copy(prevProjectionView, camera.projectionView) - Mat4.copy(prevSceneView, scene.view) + const cameraChanged = camera.updateMatrices(); + + if (force || cameraChanged) { + switch (variant) { + case 'pickObject': objectPickTarget.bind(); break; + case 'pickInstance': instancePickTarget.bind(); break; + case 'pickGroup': groupPickTarget.bind(); break; + case 'draw': + webgl.unbindFramebuffer(); + renderer.setViewport(0, 0, canvas.width, canvas.height); + break; + } + renderer.render(scene, variant) if (variant === 'draw') { - lastRenderTime = performance.now() + lastRenderTime = now() pickDirty = true } didRender = true } - return didRender + + return didRender && cameraChanged; } + let forceNextDraw = false; + function draw(force?: boolean) { - if (render('draw', force)) { - didDraw.next(performance.now() - startTime) + if (render('draw', !!force || forceNextDraw)) { + didDraw.next(now() - startTime as now.Timestamp) } + forceNextDraw = false; drawPending = false } function requestDraw(force?: boolean) { if (drawPending) return drawPending = true - window.requestAnimationFrame(() => draw(force)) + forceNextDraw = !!force; + // The animation frame is being requested by animate already. + // window.requestAnimationFrame(() => draw(force)) } function animate() { draw(false) - if (performance.now() - lastRenderTime > 200) { + if (now() - lastRenderTime > 200) { if (pickDirty) pick() } - window.requestAnimationFrame(() => animate()) + window.requestAnimationFrame(animate) } function pick() { @@ -291,11 +294,6 @@ namespace Canvas3D { return { webgl, - center: (p: Vec3) => { - Vec3.set(controls.target, p[0], p[1], p[2]) - Vec3.set(camera.target, p[0], p[1], p[2]) - }, - hide: (repr: Representation.Any) => { const renderObjectSet = reprRenderObjects.get(repr) if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = false) @@ -328,7 +326,7 @@ namespace Canvas3D { scene.clear() }, - draw, + // draw, requestDraw, animate, pick, @@ -352,12 +350,10 @@ namespace Canvas3D { case 'pickGroup': return groupPickTarget.getImageData() } }, - reprCount, - identified, didDraw, setProps: (props: Partial<Canvas3DProps>) => { - if (props.cameraMode !== undefined && props.cameraMode !== camera.mode) { - camera.mode = props.cameraMode + if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) { + camera.setState({ mode: props.cameraMode }) } if (props.backgroundColor !== undefined && props.backgroundColor !== renderer.props.clearColor) { renderer.setClearColor(props.backgroundColor) @@ -368,7 +364,7 @@ namespace Canvas3D { get props() { return { cameraPosition: Vec3.clone(camera.position), - cameraMode: camera.mode, + cameraMode: camera.state.mode, backgroundColor: renderer.props.clearColor } }, @@ -383,6 +379,7 @@ namespace Canvas3D { input.dispose() controls.dispose() renderer.dispose() + camera.dispose() } } @@ -399,6 +396,4 @@ namespace Canvas3D { groupPickTarget.setSize(pickWidth, pickHeight) } } -} - -export default Canvas3D \ No newline at end of file +} \ No newline at end of file diff --git a/src/mol-canvas3d/controls/trackball.ts b/src/mol-canvas3d/controls/trackball.ts index c88dda597536a9d091e70c38178f7f89fce0d80f..9703eac2d106fba7c1e787fd46cf3ab27c625be8 100644 --- a/src/mol-canvas3d/controls/trackball.ts +++ b/src/mol-canvas3d/controls/trackball.ts @@ -2,6 +2,7 @@ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author David Sehnal <david.sehnal@gmail.com> */ /* @@ -16,7 +17,6 @@ import { Object3D } from 'mol-gl/object3d'; export const DefaultTrackballControlsProps = { noScroll: true, - target: [0, 0, 0] as Vec3, rotateSpeed: 3.0, zoomSpeed: 4.0, @@ -25,14 +25,13 @@ export const DefaultTrackballControlsProps = { staticMoving: true, dynamicDampingFactor: 0.2, - minDistance: 0, + minDistance: 0.01, maxDistance: Infinity } export type TrackballControlsProps = Partial<typeof DefaultTrackballControlsProps> interface TrackballControls { viewport: Viewport - target: Vec3 dynamicDampingFactor: number rotateSpeed: number @@ -45,11 +44,11 @@ interface TrackballControls { } namespace TrackballControls { - export function create (input: InputObserver, object: Object3D, props: TrackballControlsProps = {}): TrackballControls { + export function create (input: InputObserver, object: Object3D & { target: Vec3 }, props: TrackballControlsProps = {}): TrackballControls { const p = { ...DefaultTrackballControlsProps, ...props } const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 } - const target: Vec3 = Vec3.clone(p.target) + const target: Vec3 = object.target let { rotateSpeed, zoomSpeed, panSpeed } = p let { staticMoving, dynamicDampingFactor } = p @@ -294,7 +293,6 @@ namespace TrackballControls { return { viewport, - target, get dynamicDampingFactor() { return dynamicDampingFactor }, set dynamicDampingFactor(value: number ) { dynamicDampingFactor = value }, diff --git a/src/mol-geo/geometry/picking.ts b/src/mol-geo/geometry/picking.ts index ac145f4b1907658d6a512d5d5440ec25fbae8619..a42ae76ee567993c03282a46a6fd7ed4237edd04 100644 --- a/src/mol-geo/geometry/picking.ts +++ b/src/mol-geo/geometry/picking.ts @@ -21,6 +21,12 @@ export interface PickingId { groupId: number } +export namespace PickingId { + export function areSame(a: PickingId, b: PickingId) { + return a.objectId === b.objectId && a.instanceId === b.instanceId && a.groupId === b.groupId; + } +} + export interface PickingInfo { label: string data?: any diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts index b564c9076680c2eadeb846ec3cf44a2f65e815ea..06cf3256925a27bc717b7529cb3529368bdec630 100644 --- a/src/mol-gl/_spec/renderer.spec.ts +++ b/src/mol-gl/_spec/renderer.spec.ts @@ -6,7 +6,7 @@ import { createGl } from './gl.shim'; -import { PerspectiveCamera } from 'mol-canvas3d/camera/perspective'; +import { Camera } from 'mol-canvas3d/camera'; import { Vec3, Mat4 } from 'mol-math/linear-algebra'; import { ValueCell } from 'mol-util'; @@ -36,7 +36,7 @@ import { Sphere3D } from 'mol-math/geometry'; function createRenderer(gl: WebGLRenderingContext) { const ctx = createContext(gl) - const camera = PerspectiveCamera.create({ + const camera = new Camera({ near: 0.01, far: 10000, position: Vec3.create(0, 0, 50) diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 4234cc38fb50fc17c1b144e2750d4c2a28049e54..9050d0cf5e0669988e7eb6cd60dc74bd6ae21a8f 100644 --- a/src/mol-gl/renderer.ts +++ b/src/mol-gl/renderer.ts @@ -6,7 +6,7 @@ // import { Vec3, Mat4 } from 'mol-math/linear-algebra' import { Viewport } from 'mol-canvas3d/camera/util'; -import { Camera } from 'mol-canvas3d/camera/base'; +import { Camera } from 'mol-canvas3d/camera'; import Scene from './scene'; import { WebGLContext, createImageData } from './webgl/context'; @@ -100,8 +100,8 @@ namespace Renderer { uHighlightColor: ValueCell.create(Vec3.clone(highlightColor)), uSelectColor: ValueCell.create(Vec3.clone(selectColor)), - uFogNear: ValueCell.create(camera.near), - uFogFar: ValueCell.create(camera.far / 50), + uFogNear: ValueCell.create(camera.state.near), + uFogFar: ValueCell.create(camera.state.far / 50), uFogColor: ValueCell.create(Vec3.clone(fogColor)), } @@ -157,8 +157,8 @@ namespace Renderer { ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection)) ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection)) - ValueCell.update(globalUniforms.uFogFar, camera.fogFar) - ValueCell.update(globalUniforms.uFogNear, camera.fogNear) + ValueCell.update(globalUniforms.uFogFar, camera.state.fogFar) + ValueCell.update(globalUniforms.uFogNear, camera.state.fogNear) currentProgramId = -1 diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 8e0b1c7157a542549cfd05a46eac617072a00c8a..054e7ea69fce89b7fe2b90abd44034de2e8a2e4d 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -37,4 +37,4 @@ export function areLociEqual(lociA: Loci, lociB: Loci) { return false } -export type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci \ No newline at end of file +export type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci \ No newline at end of file diff --git a/src/mol-model/shape/shape.ts b/src/mol-model/shape/shape.ts index 95ab6cd63d1bc859775aa14cd5203b98c145a406..6ded3bec5c76b51c45689713bfaabb7f63d2f2fa 100644 --- a/src/mol-model/shape/shape.ts +++ b/src/mol-model/shape/shape.ts @@ -25,7 +25,7 @@ export namespace Shape { let currentGroupCount = -1 return { - id: UUID.create(), + id: UUID.create22(), name, mesh, get groupCount() { diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model/structure/model/formats/mmcif.ts index 414de8fbcc6f287ccbb7548428ba18437a852848..155a94777881da609608b77cea9d6a0620d21446 100644 --- a/src/mol-model/structure/model/formats/mmcif.ts +++ b/src/mol-model/structure/model/formats/mmcif.ts @@ -169,7 +169,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities if (previous && atomic.sameAsPrevious) { return { ...previous, - id: UUID.create(), + id: UUID.create22(), modelNum: atom_site.pdbx_PDB_model_num.value(0), atomicConformation: atomic.conformation, _dynamicPropertyData: Object.create(null) @@ -182,7 +182,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities : format.data._name; return { - id: UUID.create(), + id: UUID.create22(), label, sourceData: format, modelNum: atom_site.pdbx_PDB_model_num.value(0), @@ -208,7 +208,7 @@ function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatD const coarse = getIHMCoarse(data, formatData); return { - id: UUID.create(), + id: UUID.create22(), label: data.model_name, sourceData: format, modelNum: data.model_id, diff --git a/src/mol-model/structure/model/formats/mmcif/atomic.ts b/src/mol-model/structure/model/formats/mmcif/atomic.ts index 23879400f211ce9a1b0f225a68206b65a767c1b5..20bef52965e7057bb0b99f6a3490c577064b0857 100644 --- a/src/mol-model/structure/model/formats/mmcif/atomic.ts +++ b/src/mol-model/structure/model/formats/mmcif/atomic.ts @@ -62,7 +62,7 @@ function createHierarchyData(atom_site: AtomSite, offsets: { residues: ArrayLike function getConformation(atom_site: AtomSite): AtomicConformation { return { - id: UUID.create(), + id: UUID.create22(), atomId: atom_site.id, occupancy: atom_site.occupancy, B_iso_or_equiv: atom_site.B_iso_or_equiv, diff --git a/src/mol-model/structure/model/formats/mmcif/ihm.ts b/src/mol-model/structure/model/formats/mmcif/ihm.ts index 4a331561302935489633992057edaa3b62925630..8cba685fb7c13ecaed3ca2c39cb91851b0d81534 100644 --- a/src/mol-model/structure/model/formats/mmcif/ihm.ts +++ b/src/mol-model/structure/model/formats/mmcif/ihm.ts @@ -49,7 +49,7 @@ export function getIHMCoarse(data: IHMData, formatData: FormatData): { hierarchy gaussians: { ...gaussianData, ...gaussianKeys, ...gaussianRanges }, }, conformation: { - id: UUID.create(), + id: UUID.create22(), spheres: sphereConformation, gaussians: gaussianConformation } diff --git a/src/mol-model/structure/model/properties/custom/descriptor.ts b/src/mol-model/structure/model/properties/custom/descriptor.ts index 3dcb5d9f31f5262fa3de500a2acb051f8897940c..1d2abb21c712dc17dcfd8129e3cf6faf7abc31c7 100644 --- a/src/mol-model/structure/model/properties/custom/descriptor.ts +++ b/src/mol-model/structure/model/properties/custom/descriptor.ts @@ -31,7 +31,7 @@ function ModelPropertyDescriptor<Ctx, Desc extends ModelPropertyDescriptor<Ctx>> namespace ModelPropertyDescriptor { export function getUUID(prop: ModelPropertyDescriptor): UUID { if (!(prop as any).__key) { - (prop as any).__key = UUID.create(); + (prop as any).__key = UUID.create22(); } return (prop as any).__key; } diff --git a/src/mol-model/structure/model/properties/custom/indexed.ts b/src/mol-model/structure/model/properties/custom/indexed.ts index 2276339bf66afb78cc6698bcde4f91015a81858f..9c5ee6cebbffd75a1bcca0234f627f6b15d4f685 100644 --- a/src/mol-model/structure/model/properties/custom/indexed.ts +++ b/src/mol-model/structure/model/properties/custom/indexed.ts @@ -83,7 +83,7 @@ function arrayToMap<Idx extends IndexedCustomProperty.Index, T>(array: ArrayLike } class SegmentedMappedIndexedCustomProperty<Idx extends IndexedCustomProperty.Index, T = any> implements IndexedCustomProperty<Idx, T> { - readonly id: UUID = UUID.create(); + readonly id: UUID = UUID.create22(); readonly kind: Unit.Kind; has(idx: Idx): boolean { return this.map.has(idx); } get(idx: Idx) { return this.map.get(idx); } @@ -129,7 +129,7 @@ class SegmentedMappedIndexedCustomProperty<Idx extends IndexedCustomProperty.Ind } class ElementMappedCustomProperty<T = any> implements IndexedCustomProperty<ElementIndex, T> { - readonly id: UUID = UUID.create(); + readonly id: UUID = UUID.create22(); readonly kind: Unit.Kind; readonly level = 'atom'; has(idx: ElementIndex): boolean { return this.map.has(idx); } @@ -173,7 +173,7 @@ class ElementMappedCustomProperty<T = any> implements IndexedCustomProperty<Elem } class EntityMappedCustomProperty<T = any> implements IndexedCustomProperty<EntityIndex, T> { - readonly id: UUID = UUID.create(); + readonly id: UUID = UUID.create22(); readonly kind: Unit.Kind; readonly level = 'entity'; has(idx: EntityIndex): boolean { return this.map.has(idx); } diff --git a/src/mol-model/structure/query/context.ts b/src/mol-model/structure/query/context.ts index 7bc0e6d68093ee0205c84bcf7189233b77ac6246..9dd28a6b265bc17e9c38a32af4839af32fc9ed66 100644 --- a/src/mol-model/structure/query/context.ts +++ b/src/mol-model/structure/query/context.ts @@ -5,7 +5,7 @@ */ import { Structure, StructureElement, Unit } from '../structure'; -import { now } from 'mol-task'; +import { now } from 'mol-util/now'; import { ElementIndex } from '../model'; import { Link } from '../structure/unit/links'; diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts index db04070ecb343ec2c46b18dff1d545a6c8569b9b..307b3f134810838f99ba3490fc31c37ddea1b6d0 100644 --- a/src/mol-plugin/behavior.ts +++ b/src/mol-plugin/behavior.ts @@ -5,10 +5,19 @@ */ export * from './behavior/behavior' -import * as Data from './behavior/data' -import * as Representation from './behavior/representation' + +import * as StaticState from './behavior/static/state' +import * as StaticRepresentation from './behavior/static/representation' +import * as StaticCamera from './behavior/static/camera' + +import * as DynamicRepresentation from './behavior/dynamic/representation' + +export const BuiltInPluginBehaviors = { + State: StaticState, + Representation: StaticRepresentation, + Camera: StaticCamera +} export const PluginBehaviors = { - Data, - Representation + Representation: DynamicRepresentation } \ No newline at end of file diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index ac667eee751a42cf025f9f62e41400a91f27343c..f15d4fcfd5b719c0f9a1aa307e3aff1910105107 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -4,8 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { PluginStateTransform } from '../state/base'; -import { PluginStateObjects as SO } from '../state/objects'; +import { PluginStateTransform, PluginStateObject } from '../state/objects'; import { Transformer } from 'mol-state'; import { Task } from 'mol-task'; import { PluginContext } from 'mol-plugin/context'; @@ -23,26 +22,34 @@ interface PluginBehavior<P = unknown> { } namespace PluginBehavior { + export class Root extends PluginStateObject.Create({ name: 'Root', typeClass: 'Root' }) { } + export class Behavior extends PluginStateObject.CreateBehavior<PluginBehavior>({ name: 'Behavior' }) { } + export interface Ctor<P = undefined> { new(ctx: PluginContext, params?: P): PluginBehavior<P> } export interface CreateParams<P> { name: string, ctor: Ctor<P>, label?: (params: P) => { label: string, description?: string }, - display: { name: string, description?: string }, - params?: Transformer.Definition<SO.BehaviorRoot, SO.Behavior, P>['params'] + display: { + name: string, + group: string, + description?: string + }, + params?: Transformer.Definition<Root, Behavior, P>['params'], } export function create<P>(params: CreateParams<P>) { - return PluginStateTransform.Create<SO.BehaviorRoot, SO.Behavior, P>({ + // TODO: cache groups etc + return PluginStateTransform.Create<Root, Behavior, P>({ name: params.name, display: params.display, - from: [SO.BehaviorRoot], - to: [SO.Behavior], + from: [Root], + to: [Behavior], params: params.params, apply({ params: p }, ctx: PluginContext) { const label = params.label ? params.label(p) : { label: params.display.name, description: params.display.description }; - return new SO.Behavior(label, new params.ctor(ctx, p)); + return new Behavior(new params.ctor(ctx, p), label); }, update({ b, newParams }) { return Task.create('Update Behavior', async () => { diff --git a/src/mol-plugin/behavior/camera.ts b/src/mol-plugin/behavior/camera.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/mol-plugin/behavior/data.ts b/src/mol-plugin/behavior/data.ts deleted file mode 100644 index 730e3c2176e6e074b09188ca950659e5f5352f10..0000000000000000000000000000000000000000 --- a/src/mol-plugin/behavior/data.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { PluginBehavior } from './behavior'; -import { PluginCommands } from 'mol-plugin/command'; -import { StateTree } from 'mol-state'; - -export const SetCurrentObject = PluginBehavior.create({ - name: 'set-current-data-object-behavior', - ctor: PluginBehavior.simpleCommandHandler(PluginCommands.Data.SetCurrentObject, ({ ref }, ctx) => ctx.state.data.setCurrent(ref)), - display: { name: 'Set Current Handler' } -}); - -export const Update = PluginBehavior.create({ - name: 'update-data-behavior', - ctor: PluginBehavior.simpleCommandHandler(PluginCommands.Data.Update, ({ tree }, ctx) => ctx.state.updateData(tree)), - display: { name: 'Update Data Handler' } -}); - -export const RemoveObject = PluginBehavior.create({ - name: 'remove-object-data-behavior', - ctor: PluginBehavior.simpleCommandHandler(PluginCommands.Data.RemoveObject, ({ ref }, ctx) => { - const tree = StateTree.build(ctx.state.data.tree).delete(ref).getTree(); - ctx.state.updateData(tree); - }), - display: { name: 'Remove Object Handler' } -}); \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9d211670397911053a2d16bad52bcb63a717951 --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginBehavior } from '../behavior'; +import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci'; +import { MarkerAction } from 'mol-geo/geometry/marker-data'; + +export const HighlightLoci = PluginBehavior.create({ + name: 'representation-highlight-loci', + ctor: class extends PluginBehavior.Handler { + register(): void { + let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0; + this.subscribeObservable(this.ctx.behaviors.canvas.highlightLoci, current => { + if (!this.ctx.canvas3d) return; + + if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) { + this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight); + this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight); + prevLoci = current.loci; + prevRepr = current.repr; + } + }); + } + }, + display: { name: 'Highlight Loci on Canvas', group: 'Data' } +}); + +export const SelectLoci = PluginBehavior.create({ + name: 'representation-select-loci', + ctor: class extends PluginBehavior.Handler { + register(): void { + this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, ({ loci }) => { + if (!this.ctx.canvas3d) return; + this.ctx.canvas3d.mark(loci, MarkerAction.Toggle); + }); + } + }, + display: { name: 'Select Loci on Canvas', group: 'Data' } +}); \ No newline at end of file diff --git a/src/mol-plugin/behavior/representation.ts b/src/mol-plugin/behavior/representation.ts deleted file mode 100644 index f2eac5aa9da549e3c1f483dca98e6cb2ca458313..0000000000000000000000000000000000000000 --- a/src/mol-plugin/behavior/representation.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { PluginBehavior } from './behavior'; -import { PluginStateObjects as SO } from '../state/objects'; - -class _AddRepresentationToCanvas extends PluginBehavior.Handler { - register(): void { - this.subscribeObservable(this.ctx.events.state.data.object.created, o => { - if (!SO.StructureRepresentation3D.is(o.obj)) return; - this.ctx.canvas3d.add(o.obj.data); - this.ctx.canvas3d.requestDraw(true); - }); - this.subscribeObservable(this.ctx.events.state.data.object.updated, o => { - const oo = o.obj; - if (!SO.StructureRepresentation3D.is(oo)) return; - this.ctx.canvas3d.add(oo.data); - this.ctx.canvas3d.requestDraw(true); - }); - this.subscribeObservable(this.ctx.events.state.data.object.removed, o => { - const oo = o.obj; - console.log('removed', o.ref, oo && oo.type); - if (!SO.StructureRepresentation3D.is(oo)) return; - this.ctx.canvas3d.remove(oo.data); - console.log('removed from canvas', o.ref); - this.ctx.canvas3d.requestDraw(true); - oo.data.destroy(); - }); - this.subscribeObservable(this.ctx.events.state.data.object.replaced, o => { - if (o.oldObj && SO.StructureRepresentation3D.is(o.oldObj)) { - this.ctx.canvas3d.remove(o.oldObj.data); - this.ctx.canvas3d.requestDraw(true); - o.oldObj.data.destroy(); - } - if (o.newObj && SO.StructureRepresentation3D.is(o.newObj)) { - this.ctx.canvas3d.add(o.newObj.data); - this.ctx.canvas3d.requestDraw(true); - } - }); - } -} - -export const AddRepresentationToCanvas = PluginBehavior.create({ - name: 'add-representation-to-canvas', - ctor: _AddRepresentationToCanvas, - display: { name: 'Add Representation To Canvas' } -}); \ No newline at end of file diff --git a/src/mol-plugin/behavior/static/camera.ts b/src/mol-plugin/behavior/static/camera.ts new file mode 100644 index 0000000000000000000000000000000000000000..8aa1074d5a0cb96f0eed422ab4bf46552831b3da --- /dev/null +++ b/src/mol-plugin/behavior/static/camera.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +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) { + PluginCommands.Camera.Reset.subscribe(ctx, () => { + const sel = ctx.state.dataState.select(q => q.root.subtree().ofType(SO.Molecule.Structure)); + if (!sel.length) return; + + const center = (sel[0].obj! as SO.Molecule.Structure).data.boundary.sphere.center; + ctx.canvas3d.camera.setState({ target: center }); + ctx.canvas3d.requestDraw(true); + + // 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/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee5c6a0e8343ee643070d580d1f3a282d54bffb4 --- /dev/null +++ b/src/mol-plugin/behavior/static/representation.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginStateObject as SO } from '../../state/objects'; +import { PluginContext } from 'mol-plugin/context'; + +export function registerDefault(ctx: PluginContext) { + SyncRepresentationToCanvas(ctx); +} + +export function SyncRepresentationToCanvas(ctx: PluginContext) { + const events = ctx.state.dataState.events; + events.object.created.subscribe(e => { + if (!SO.isRepresentation3D(e.obj)) return; + ctx.canvas3d.add(e.obj.data); + ctx.canvas3d.requestDraw(true); + + // TODO: update visiblity + }); + events.object.updated.subscribe(e => { + if (e.oldObj && SO.isRepresentation3D(e.oldObj)) { + ctx.canvas3d.remove(e.oldObj.data); + ctx.canvas3d.requestDraw(true); + e.oldObj.data.destroy(); + } + + if (!SO.isRepresentation3D(e.obj)) return; + + // TODO: update visiblity + ctx.canvas3d.add(e.obj.data); + ctx.canvas3d.requestDraw(true); + }); + events.object.removed.subscribe(e => { + const oo = e.obj; + if (!SO.isRepresentation3D(oo)) return; + ctx.canvas3d.remove(oo.data); + ctx.canvas3d.requestDraw(true); + oo.data.destroy(); + }); +} + +export function UpdateRepresentationVisibility(ctx: PluginContext) { + ctx.state.dataState.events.cell.stateUpdated.subscribe(e => { + const cell = e.state.cells.get(e.ref)!; + if (!SO.isRepresentation3D(cell.obj)) return; + + // TODO: update visiblity + }) +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c4124f913246b77ee1a7e0356baa2585dfe07ca --- /dev/null +++ b/src/mol-plugin/behavior/static/state.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +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); + Update(ctx); + ApplyAction(ctx); + RemoveObject(ctx); + ToggleExpanded(ctx); + ToggleVisibility(ctx); + Snapshots(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)); + }); +} + +export function ToggleExpanded(ctx: PluginContext) { + PluginCommands.State.ToggleExpanded.subscribe(ctx, ({ state, ref }) => state.updateCellState(ref, ({ isCollapsed }) => ({ isCollapsed: !isCollapsed }))); +} + +export function ToggleVisibility(ctx: PluginContext) { + PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.tree.cellStates.get(ref).isHidden)); +} + +function setVisibility(state: State, root: Transform.Ref, value: boolean) { + StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), { state, value }, setVisibilityVisitor); +} + +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); + }); + + PluginCommands.State.Snapshots.Upload.subscribe(ctx, ({ name, description, serverUrl }) => { + return fetch(`${serverUrl}/set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`, { + method: 'POST', + mode: 'cors', + referrer: 'no-referrer', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + body: JSON.stringify(ctx.state.getSnapshot()) + }) as any as Promise<void>; + }); + + PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => { + const req = await fetch(url, { referrer: 'no-referrer' }); + const json = await req.json(); + return ctx.state.setSnapshot(json.data); + }); +} \ No newline at end of file diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 01d93d78264e78fd3639b75a5f323628eca49946..ec948ef858251ab75850ecb96604d26dc1040481 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -4,9 +4,12 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as Data from './command/data'; +import * as State from './command/state'; +import * as Camera from './command/camera'; export * from './command/command'; + export const PluginCommands = { - Data + State, + Camera } \ No newline at end of file diff --git a/src/mol-plugin/command/camera.ts b/src/mol-plugin/command/camera.ts new file mode 100644 index 0000000000000000000000000000000000000000..0020d13354819b81f82013011e4c26e4f455312a --- /dev/null +++ b/src/mol-plugin/command/camera.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginCommand } from './command'; +import { Camera } from 'mol-canvas3d/camera'; + +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/command/command.ts b/src/mol-plugin/command/command.ts index a76186d5aeaaaae2753c8dd3228c80a035eda76a..fcf1980a4e158b1dc29f27a7c085b5c187327095 100644 --- a/src/mol-plugin/command/command.ts +++ b/src/mol-plugin/command/command.ts @@ -7,22 +7,22 @@ import { PluginContext } from '../context'; import { LinkedList } from 'mol-data/generic'; import { RxEventHelper } from 'mol-util/rx-event-helper'; +import { UUID } from 'mol-util'; export { PluginCommand } interface PluginCommand<T = unknown> { - readonly id: PluginCommand.Id, + readonly id: UUID, dispatch(ctx: PluginContext, params: T): Promise<void>, subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription, - params?: { toJSON(params: T): any, fromJSON(json: any): T } + params: { isImmediate: boolean } } /** namespace.id must a globally unique identifier */ -function PluginCommand<T>(namespace: string, id: string, params?: PluginCommand<T>['params']): PluginCommand<T> { - return new Impl(`${namespace}.${id}` as PluginCommand.Id, params); +function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> { + return new Impl({ isImmediate: false, ...params }); } -const cmdRepo = new Map<string, PluginCommand<any>>(); class Impl<T> implements PluginCommand<T> { dispatch(ctx: PluginContext, params: T): Promise<void> { return ctx.commands.dispatch(this, params) @@ -30,9 +30,8 @@ class Impl<T> implements PluginCommand<T> { subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription { return ctx.commands.subscribe(this, action); } - constructor(public id: PluginCommand.Id, public params: PluginCommand<T>['params']) { - if (cmdRepo.has(id)) throw new Error(`Command id '${id}' already in use.`); - cmdRepo.set(id, this); + id = UUID.create22(); + constructor(public params: PluginCommand<T>['params']) { } } @@ -44,7 +43,7 @@ namespace PluginCommand { } export type Action<T> = (params: T) => void | Promise<void> - type Instance = { id: string, params: any, resolve: () => void, reject: (e: any) => void } + type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void } export class Manager { private subs = new Map<string, Action<any>[]>(); @@ -85,22 +84,27 @@ namespace PluginCommand { /** Resolves after all actions have completed */ - dispatch<T>(cmd: PluginCommand<T> | Id, params: T) { + dispatch<T>(cmd: PluginCommand<T>, params: T) { return new Promise<void>((resolve, reject) => { if (this.disposing) { reject('disposed'); return; } - const id = typeof cmd === 'string' ? cmd : (cmd as PluginCommand<T>).id; - const actions = this.subs.get(id); + const actions = this.subs.get(cmd.id); if (!actions) { resolve(); return; } - this.queue.addLast({ id, params, resolve, reject }); - this.next(); + const instance: Instance = { cmd, params, resolve, reject }; + + if (cmd.params.isImmediate) { + this.resolve(instance); + } else { + this.queue.addLast({ cmd, params, resolve, reject }); + this.next(); + } }); } @@ -111,24 +115,39 @@ namespace PluginCommand { } } - private async next() { - if (this.queue.count === 0) return; - const cmd = this.queue.removeFirst()!; - - const actions = this.subs.get(cmd.id); - if (!actions) return; + private async resolve(instance: Instance) { + const actions = this.subs.get(instance.cmd.id); + if (!actions) { + try { + instance.resolve(); + } finally { + if (!instance.cmd.params.isImmediate && !this.disposing) this.next(); + } + return; + } try { + if (!instance.cmd.params.isImmediate) this.executing = true; // TODO: should actions be called "asynchronously" ("setImmediate") instead? for (const a of actions) { - await a(cmd.params); + await a(instance.params); } - cmd.resolve(); + instance.resolve(); } catch (e) { - cmd.reject(e); + instance.reject(e); } finally { - if (!this.disposing) this.next(); + if (!instance.cmd.params.isImmediate) { + this.executing = false; + if (!this.disposing) this.next(); + } } } + + private executing = false; + private async next() { + if (this.queue.count === 0 || this.executing) return; + const instance = this.queue.removeFirst()!; + this.resolve(instance); + } } } \ No newline at end of file diff --git a/src/mol-plugin/command/data.ts b/src/mol-plugin/command/data.ts deleted file mode 100644 index b9965e7ed2b1ed252718b87f1e532e248e5f3b66..0000000000000000000000000000000000000000 --- a/src/mol-plugin/command/data.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { PluginCommand } from './command'; -import { Transform, StateTree } from 'mol-state'; - -export const SetCurrentObject = PluginCommand<{ ref: Transform.Ref }>('ms-data', 'set-current-object'); -export const Update = PluginCommand<{ tree: StateTree }>('ms-data', 'update'); -export const UpdateObject = PluginCommand<{ ref: Transform.Ref, params: any }>('ms-data', 'update-object'); -export const RemoveObject = PluginCommand<{ ref: Transform.Ref }>('ms-data', 'remove-object'); \ No newline at end of file diff --git a/src/mol-plugin/command/state.ts b/src/mol-plugin/command/state.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9b092b00e04e65e1c910d6bde8c73325de761f7 --- /dev/null +++ b/src/mol-plugin/command/state.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +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 }>(); +export const ApplyAction = PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>(); +export const Update = PluginCommand<{ state: State, tree: State.Tree | State.Builder }>(); + +// export const UpdateObject = PluginCommand<{ ref: Transform.Ref, params: any }>('ms-data', 'update-object'); + +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 }); + +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 }), + + Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>({ isImmediate: true }), + Fetch: PluginCommand<{ url: string }>() +} \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 4aca923fe16165ff7a334f495f34b99d227b2839..3a80ae51ce42397b2371fd339113f6816ae21469 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -4,36 +4,59 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StateTree, StateSelection, Transformer, Transform } from 'mol-state'; -import Canvas3D from 'mol-canvas3d/canvas3d'; +import { Transformer, Transform, State } from 'mol-state'; +import { Canvas3D } from 'mol-canvas3d/canvas3d'; import { StateTransforms } from './state/transforms'; -import { PluginStateObjects as SO } from './state/objects'; +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/representation'; +import { CreateStructureFromPDBe } from './state/actions/basic'; +import { LogEntry } from 'mol-util/log-entry'; +import { TaskManager } from './util/task-manager'; export class PluginContext { private disposed = false; private ev = RxEventHelper.create(); + private tasks = new TaskManager(); readonly state = new PluginState(this); readonly commands = new PluginCommand.Manager(); readonly events = { state: { - data: this.state.data.context.events, - behavior: this.state.behavior.context.events - } + cell: { + stateUpdated: merge(this.state.dataState.events.cell.stateUpdated, this.state.behaviorState.events.cell.stateUpdated), + created: merge(this.state.dataState.events.cell.created, this.state.behaviorState.events.cell.created), + removed: merge(this.state.dataState.events.cell.removed, this.state.behaviorState.events.cell.removed), + }, + object: { + created: merge(this.state.dataState.events.object.created, this.state.behaviorState.events.object.created), + removed: merge(this.state.dataState.events.object.removed, this.state.behaviorState.events.object.removed), + updated: merge(this.state.dataState.events.object.updated, this.state.behaviorState.events.object.updated) + }, + // data: this.state.dataState.events, + // behavior: this.state.behaviorState.events, + cameraSnapshots: this.state.cameraSnapshots.events, + snapshots: this.state.snapshots.events, + }, + log: this.ev<LogEntry>(), + task: this.tasks.events }; readonly behaviors = { - state: { - data: this.state.data.context.behaviors, - behavior: this.state.behavior.context.behaviors + // state: { + // data: this.state.dataState.behaviors, + // behavior: this.state.behaviorState.behaviors + // }, + canvas: { + highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }), + selectLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }), }, command: this.commands.behaviour }; @@ -45,14 +68,18 @@ export class PluginContext { try { (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container); this.canvas3d.animate(); - console.log('canvas3d created'); return true; } catch (e) { + this.log(LogEntry.error('' + e)); console.error(e); return false; } } + log(e: LogEntry) { + this.events.log.next(e); + } + /** * This should be used in all transform related request so that it could be "spoofed" to allow * "static" access to resources. @@ -62,8 +89,8 @@ export class PluginContext { return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer()); } - async runTask<T>(task: Task<T>) { - return await task.run(p => console.log(p), 250); + runTask<T>(task: Task<T>) { + return this.tasks.run(task); } dispose() { @@ -72,100 +99,73 @@ export class PluginContext { this.canvas3d.dispose(); this.ev.dispose(); this.state.dispose(); + this.tasks.dispose(); this.disposed = true; } - async _test_initBehaviours() { - const tree = StateTree.build(this.state.behavior.tree) - .toRoot().apply(PluginBehaviors.Data.SetCurrentObject) - .and().toRoot().apply(PluginBehaviors.Data.Update) - .and().toRoot().apply(PluginBehaviors.Data.RemoveObject) - .and().toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas) - .getTree(); + private initBuiltInBehavior() { + BuiltInPluginBehaviors.State.registerDefault(this); + BuiltInPluginBehaviors.Representation.registerDefault(this); + BuiltInPluginBehaviors.Camera.registerDefault(this); - await this.state.updateBehaviour(tree); + merge(this.state.dataState.events.log, this.state.behaviorState.events.log).subscribe(e => this.events.log.next(e)); } - _test_applyTransform(a: Transform.Ref, transformer: Transformer, params: any) { - const tree = StateTree.build(this.state.data.tree).to(a).apply(transformer, params).getTree(); - PluginCommands.Data.Update.dispatch(this, { tree }); + async _test_initBehaviors() { + const tree = this.state.behaviorState.tree.build() + .toRoot().apply(PluginBehaviors.Representation.HighlightLoci, { ref: PluginBehaviors.Representation.HighlightLoci.id }) + .toRoot().apply(PluginBehaviors.Representation.SelectLoci, { ref: PluginBehaviors.Representation.SelectLoci.id }) + .getTree(); + + await this.runTask(this.state.behaviorState.update(tree)); } - _test_createState(url: string) { - const b = StateTree.build(this.state.data.tree); - - 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' - ]) - }); + _test_initDataActions() { + this.state.dataState.actions + .add(CreateStructureFromPDBe) + .add(StateTransforms.Data.Download) + .add(StateTransforms.Data.ParseCif) + .add(StateTransforms.Model.CreateStructureAssembly) + .add(StateTransforms.Model.CreateStructure) + .add(StateTransforms.Model.CreateModelFromTrajectory) + .add(StateTransforms.Visuals.CreateStructureRepresentation); + } - const newTree = b.toRoot() - .apply(StateTransforms.Data.Download, { url }) - .apply(StateTransforms.Data.ParseCif) - .apply(StateTransforms.Model.ParseModelsFromMmCif, {}, { ref: 'models' }) - .apply(StateTransforms.Model.CreateStructureFromModel, { modelIndex: 0 }, { ref: 'structure' }) - .apply(StateTransforms.Model.CreateStructureAssembly) - .apply(StateTransforms.Model.CreateStructureSelection, { query, label: 'ALA residues' }) - .apply(StateTransforms.Visuals.CreateStructureRepresentation) - .getTree(); + 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 }); + } - this.state.updateData(newTree); + updateTransform(state: State, a: Transform.Ref, params: any) { + const tree = state.build().to(a).update(params); + return PluginCommands.State.Update.dispatch(this, { state, tree }); } private initEvents() { - merge(this.events.state.data.object.created, this.events.state.behavior.object.created).subscribe(o => { - console.log('creating', o.obj.type); - if (!SO.Behavior.is(o.obj)) return; + this.events.state.object.created.subscribe(o => { + if (!SO.isBehavior(o.obj)) return; o.obj.data.register(); }); - merge(this.events.state.data.object.removed, this.events.state.behavior.object.removed).subscribe(o => { - if (!SO.Behavior.is(o.obj)) return; + this.events.state.object.removed.subscribe(o => { + if (!SO.isBehavior(o.obj)) return; o.obj.data.unregister(); }); - merge(this.events.state.data.object.replaced, this.events.state.behavior.object.replaced).subscribe(o => { - if (o.oldObj && SO.Behavior.is(o.oldObj)) o.oldObj.data.unregister(); - if (o.newObj && SO.Behavior.is(o.newObj)) o.newObj.data.register(); + this.events.state.object.updated.subscribe(o => { + if (o.action === 'recreate') { + if (o.oldObj && SO.isBehavior(o.oldObj)) o.oldObj.data.unregister(); + if (o.obj && SO.isBehavior(o.obj)) o.obj.data.register(); + } }); } - _test_centerView() { - const sel = StateSelection.select(StateSelection.root().subtree().ofType(SO.Structure.type), this.state.data); - if (!sel.length) return; - - const center = (sel[0].obj! as SO.Structure).data.boundary.sphere.center; - console.log({ sel, center, rc: this.canvas3d.reprCount }); - this.canvas3d.center(center); - this.canvas3d.requestDraw(true); - } - - _test_nextModel() { - const models = StateSelection.select('models', this.state.data)[0].obj as SO.Models; - const idx = (this.state.data.tree.getValue('structure')!.params as Transformer.Params<typeof StateTransforms.Model.CreateStructureFromModel>).modelIndex; - const newTree = StateTree.updateParams(this.state.data.tree, 'structure', { modelIndex: (idx + 1) % models.data.length }); - return this.state.updateData(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/index.ts b/src/mol-plugin/index.ts index f54fd023beeac8facb7b407428a9a7f352c82722..491b65dfe9fcca732808ddb8d9dca47513ed5164 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -8,6 +8,7 @@ import { PluginContext } from './context'; import { Plugin } from './ui/plugin' import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { PluginCommands } from './command'; function getParam(name: string, regex: string): string { let r = new RegExp(`${name}=(${regex})[&]?`, 'i'); @@ -28,8 +29,9 @@ export function createPlugin(target: HTMLElement): PluginContext { } function trySetSnapshot(ctx: PluginContext) { - const snapshot = getParam('snapshot', `(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?`); - if (!snapshot) return; - const data = JSON.parse(atob(snapshot)); - setTimeout(() => ctx.state.setSnapshot(data), 250); + const snapshotUrl = getParam('snapshot-url', `[^&]+`); + if (!snapshotUrl) return; + // const data = JSON.parse(atob(snapshot)); + // setTimeout(() => ctx.state.setSnapshot(data), 250); + PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url: snapshotUrl }) } \ No newline at end of file diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index a6892cba4e1ace0181b003bff2bad9faca577219..489c9477d831e9644ea3eb4e9a1fcc520cb032d1 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -4,64 +4,89 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { State, StateTree } from 'mol-state'; -import { PluginStateObjects as SO } from './state/objects'; -import { CombinedCamera } from 'mol-canvas3d/camera/combined'; - +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'; +import { PluginStateSnapshotManager } from './state/snapshots'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; export { PluginState } class PluginState { - readonly data: State; - readonly behavior: State; + private ev = RxEventHelper.create(); + + readonly dataState: State; + readonly behaviorState: State; + readonly cameraSnapshots = new CameraSnapshotManager(); + + readonly snapshots = new PluginStateSnapshotManager(); + + readonly behavior = { + kind: this.ev.behavior<PluginState.Kind>('data'), + currentObject: this.ev.behavior<State.ObjectEvent>({} as any) + } + + setKind(kind: PluginState.Kind) { + const current = this.behavior.kind.value; + if (kind !== current) { + this.behavior.kind.next(kind); + this.behavior.currentObject.next(kind === 'data' + ? this.dataState.behaviors.currentObject.value + : this.behaviorState.behaviors.currentObject.value) + } + } getSnapshot(): PluginState.Snapshot { return { - data: this.data.getSnapshot(), - behaviour: this.behavior.getSnapshot(), + data: this.dataState.getSnapshot(), + behaviour: this.behaviorState.getSnapshot(), + cameraSnapshots: this.cameraSnapshots.getStateSnapshot(), canvas3d: { - camera: { ...this.plugin.canvas3d.camera } + camera: this.plugin.canvas3d.camera.getSnapshot() } }; } async setSnapshot(snapshot: PluginState.Snapshot) { - await this.behavior.setSnapshot(snapshot.behaviour); - await this.data.setSnapshot(snapshot.data); - - // TODO: handle camera - // console.log({ old: { ...this.plugin.canvas3d.camera }, new: snapshot.canvas3d.camera }); - // CombinedCamera.copy(snapshot.canvas3d.camera, this.plugin.canvas3d.camera); - // CombinedCamera.update(this.plugin.canvas3d.camera); - // this.plugin.canvas3d.center - // console.log({ copied: { ...this.plugin.canvas3d.camera } }); + // await this.plugin.runTask(this.behaviorState.setSnapshot(snapshot.behaviour)); + await this.plugin.runTask(this.dataState.setSnapshot(snapshot.data)); + this.cameraSnapshots.setStateSnapshot(snapshot.cameraSnapshots); + this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera); this.plugin.canvas3d.requestDraw(true); - // console.log('updated camera'); - } - - updateData(tree: StateTree) { - return this.plugin.runTask(this.data.update(tree)); - } - - updateBehaviour(tree: StateTree) { - return this.plugin.runTask(this.behavior.update(tree)); } dispose() { - this.data.dispose(); + this.ev.dispose(); + this.dataState.dispose(); + this.behaviorState.dispose(); + this.cameraSnapshots.dispose(); } constructor(private plugin: import('./context').PluginContext) { - this.data = State.create(new SO.DataRoot({ label: 'Root' }, { }), { globalContext: plugin }); - this.behavior = State.create(new SO.BehaviorRoot({ label: 'Root' }, { }), { globalContext: plugin }); + this.dataState = State.create(new SO.Root({ }), { globalContext: plugin }); + this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin }); + + this.dataState.behaviors.currentObject.subscribe(o => { + if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o); + }); + this.behaviorState.behaviors.currentObject.subscribe(o => { + if (this.behavior.kind.value === 'behavior') this.behavior.currentObject.next(o); + }); + + this.behavior.currentObject.next(this.dataState.behaviors.currentObject.value); } } namespace PluginState { + export type Kind = 'data' | 'behavior' + export interface Snapshot { data: State.Snapshot, behaviour: State.Snapshot, + cameraSnapshots: CameraSnapshotManager.StateSnapshot, canvas3d: { - camera: CombinedCamera + camera: Camera.Snapshot } } } diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index 11efaeae59cd1b93481163252f7925f02a1372a5..5fdfdbc2ca4eea7ad4c06db994742426cbc10ad1 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/basic.ts @@ -4,4 +4,86 @@ * @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'), + }), + validate: p => !p.id || !p.id.trim() ? ['Enter id.'] : void 0 + }, + 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.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: 'Update Trajectory' + }, + 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/base.ts b/src/mol-plugin/state/base.ts deleted file mode 100644 index 5b7db3fc0583f5f62c809f6b19e1159632680ec1..0000000000000000000000000000000000000000 --- a/src/mol-plugin/state/base.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { StateObject, Transformer } from 'mol-state'; - -export type TypeClass = 'root' | 'data' | 'prop' - -export namespace PluginStateObject { - export type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation' | 'Behavior' - export interface TypeInfo { name: string, shortName: string, description: string, typeClass: TypeClass } - export interface Props { label: string, description?: string } - - export const Create = StateObject.factory<TypeInfo, Props>(); -} - -export namespace PluginStateTransform { - export const Create = Transformer.factory('ms-plugin'); -} \ No newline at end of file diff --git a/src/mol-plugin/state/camera.ts b/src/mol-plugin/state/camera.ts new file mode 100644 index 0000000000000000000000000000000000000000..7874dbd7b7355d414c34e5cb42eb64b7742720b3 --- /dev/null +++ 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/state/objects.ts b/src/mol-plugin/state/objects.ts index 93b8aa819498e9030408fb4a46e6a6b87fb24515..24347d8c0a1d1856036b4ee43286a3b9aaa5218f 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -4,38 +4,70 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { PluginStateObject } from './base'; import { CifFile } from 'mol-io/reader/cif'; -import { Model as _Model, Structure as _Structure } from 'mol-model/structure' +import { Model as _Model, Structure as _Structure } from 'mol-model/structure'; +import { VolumeData } from 'mol-model/volume'; +import { PluginBehavior } from 'mol-plugin/behavior/behavior'; +import { Representation } from 'mol-repr/representation'; import { StructureRepresentation } from 'mol-repr/structure/representation'; +import { VolumeRepresentation } from 'mol-repr/volume/representation'; +import { StateObject, Transformer } from 'mol-state'; -const _create = PluginStateObject.Create +export type TypeClass = 'root' | 'data' | 'prop' -namespace PluginStateObjects { - export class DataRoot extends _create({ name: 'Root', shortName: 'R', typeClass: 'Root', description: 'Where everything begins.' }) { } - export class BehaviorRoot extends _create({ name: 'Root', shortName: 'R', typeClass: 'Root', description: 'Where everything begins.' }) { } +export namespace PluginStateObject { + export type Any = StateObject<any, TypeInfo> - export class Group extends _create({ name: 'Group', shortName: 'G', typeClass: 'Group', description: 'A group on entities.' }) { } + export type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation3D' | 'Behavior' + export interface TypeInfo { name: string, typeClass: TypeClass } - export class Behavior extends _create<import('../behavior').PluginBehavior>({ name: 'Behavior', shortName: 'B', typeClass: 'Behavior', description: 'Modifies plugin functionality.' }) { } + export const Create = StateObject.factory<TypeInfo>(); + + export function isRepresentation3D(o?: Any): o is StateObject<Representation.Any, TypeInfo> { + return !!o && o.type.typeClass === 'Representation3D'; + } + + export function isBehavior(o?: Any): o is StateObject<PluginBehavior, TypeInfo> { + return !!o && o.type.typeClass === 'Behavior'; + } + + export function CreateRepresentation3D<T extends Representation.Any>(type: { name: string }) { + return Create<T>({ ...type, typeClass: 'Representation3D' }) + } + + export function CreateBehavior<T extends PluginBehavior>(type: { name: string }) { + return Create<T>({ ...type, typeClass: 'Behavior' }) + } + + export class Root extends Create({ name: 'Root', typeClass: 'Root' }) { } + + export class Group extends Create({ name: 'Group', typeClass: 'Group' }) { } export namespace Data { - export class String extends _create<string>({ name: 'String Data', typeClass: 'Data', shortName: 'S_D', description: 'A string.' }) { } - export class Binary extends _create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data', shortName: 'B_D', description: 'A binary blob.' }) { } - export class Json extends _create<any>({ name: 'JSON Data', typeClass: 'Data', shortName: 'JS_D', description: 'Represents JSON data.' }) { } - export class Cif extends _create<CifFile>({ name: 'Cif File', typeClass: 'Data', shortName: 'CF', description: 'Represents parsed CIF data.' }) { } + export class String extends Create<string>({ name: 'String Data', typeClass: 'Data', }) { } + export class Binary extends Create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data' }) { } + export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } + export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } // TODO - // export class MultipleRaw extends _create<{ + // export class MultipleRaw extends Create<{ // [key: string]: { type: 'String' | 'Binary', data: string | Uint8Array } // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { } } - export class Models extends _create<ReadonlyArray<_Model>>({ name: 'Molecule Model', typeClass: 'Object', shortName: 'M_M', description: 'A model of a molecule.' }) { } - export class Structure extends _create<_Structure>({ name: 'Molecule Structure', typeClass: 'Object', shortName: 'M_S', description: 'A structure of a molecule.' }) { } - + export namespace Molecule { + export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { } + export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { } + export class Structure extends Create<_Structure>({ name: 'Structure', typeClass: 'Object' }) { } + export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any>>({ name: 'Structure 3D' }) { } + } - export class StructureRepresentation3D extends _create<StructureRepresentation<any>>({ name: 'Molecule Structure Representation', typeClass: 'Representation', shortName: 'S_R', description: 'A representation of a molecular structure.' }) { } + export namespace Volume { + export class Data extends Create<VolumeData>({ name: 'Volume Data', typeClass: 'Object' }) { } + export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>>({ name: 'Volume 3D' }) { } + } } -export { PluginStateObjects } \ No newline at end of file +export namespace PluginStateTransform { + export const Create = Transformer.factory('ms-plugin'); +} \ No newline at end of file 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/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index 67899a9c666663370ce38be4717ef5d37428776e..a3379657811969c19913feae848307dc058f74b5 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -4,22 +4,23 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { PluginStateTransform } from '../base'; -import { PluginStateObjects as SO } from '../objects'; +import { PluginStateTransform } from '../objects'; +import { PluginStateObject as SO } from '../objects'; import { Task } from 'mol-task'; import CIF from 'mol-io/reader/cif' import { PluginContext } from 'mol-plugin/context'; import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { Transformer } from 'mol-state'; export { Download } namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } } -const Download = PluginStateTransform.Create<SO.DataRoot, SO.Data.String | SO.Data.Binary, Download.Params>({ +const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, Download.Params>({ name: 'download', display: { name: 'Download', description: 'Download string or binary data from the specified URL' }, - from: [SO.DataRoot], + from: [SO.Root], to: [SO.Data.String, SO.Data.Binary], params: { default: () => ({ @@ -27,17 +28,27 @@ const Download = PluginStateTransform.Create<SO.DataRoot, SO.Data.String | SO.Da }), controls: () => ({ url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''), + label: PD.Text('Label', '', ''), isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false) - }) + }), + validate: p => !p.url || !p.url.trim() ? ['Enter url.'] : void 0 }, apply({ params: p }, globalCtx: PluginContext) { return Task.create('Download', async ctx => { // TODO: track progress const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string'); return p.isBinary - ? new SO.Data.Binary({ label: p.label ? p.label : p.url }, data as Uint8Array) - : new SO.Data.String({ label: p.label ? p.label : p.url }, data as string); + ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.url }) + : new SO.Data.String(data as string, { label: p.label ? p.label : p.url }); }); + }, + update({ oldParams, newParams, b }) { + if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return Transformer.UpdateResult.Recreate; + if (oldParams.label !== newParams.label) { + (b.label as string) = newParams.label || newParams.url; + return Transformer.UpdateResult.Updated; + } + return Transformer.UpdateResult.Unchanged; } }); @@ -55,7 +66,7 @@ const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO return Task.create('Parse CIF', async ctx => { const parsed = await (SO.Data.String.is(a) ? CIF.parse(a.data) : CIF.parseBinary(a.data)).runInContext(ctx); if (parsed.isError) throw new Error(parsed.message); - return new SO.Data.Cif({ label: 'CIF File' }, parsed.result); + return new SO.Data.Cif(parsed.result); }); } }); \ 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 1ebe478e28a9ffcd0dff29c587c5aac811bb401d..93b5907132aa1f2a9675901878dca5a59ddf7479 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -4,8 +4,8 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { PluginStateTransform } from '../base'; -import { PluginStateObjects as SO } from '../objects'; +import { PluginStateTransform } from '../objects'; +import { PluginStateObject as SO } from '../objects'; import { Task } from 'mol-task'; import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection } from 'mol-model/structure'; import { ParamDefinition as PD } from 'mol-util/param-definition'; @@ -13,16 +13,16 @@ import Expression from 'mol-script/language/expression'; import { compile } from 'mol-script/runtime/query/compiler'; import { Mat4 } from 'mol-math/linear-algebra'; -export { ParseModelsFromMmCif } -namespace ParseModelsFromMmCif { export interface Params { blockHeader?: string } } -const ParseModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, ParseModelsFromMmCif.Params>({ - name: 'parse-models-from-mmcif', +export { ParseTrajectoryFromMmCif } +namespace ParseTrajectoryFromMmCif { export interface Params { blockHeader?: string } } +const ParseTrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule.Trajectory, ParseTrajectoryFromMmCif.Params>({ + name: 'parse-trajectory-from-mmcif', display: { name: 'Models from mmCIF', description: 'Identify and create all separate models in the specified CIF data block' }, from: [SO.Data.Cif], - to: [SO.Models], + to: [SO.Molecule.Trajectory], params: { default: a => ({ blockHeader: a.data.blocks[0].header }), controls(a) { @@ -40,22 +40,22 @@ const ParseModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, if (!block) throw new Error(`Data block '${[header]}' not found.`); const models = await Model.create(Format.mmCIF(block)).runInContext(ctx); if (models.length === 0) throw new Error('No models found.'); - const label = models.length === 1 ? `${models[0].label}` : `${models[0].label} (${models.length} models)`; - return new SO.Models({ label }, models); + const label = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` }; + return new SO.Molecule.Trajectory(models, label); }); } }); -export { CreateStructureFromModel } -namespace CreateStructureFromModel { export interface Params { modelIndex: number, transform3d?: Mat4 } } -const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, CreateStructureFromModel.Params>({ - name: 'create-structure-from-model', +export { CreateModelFromTrajectory } +namespace CreateModelFromTrajectory { export interface Params { modelIndex: number } } +const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, SO.Molecule.Model, CreateModelFromTrajectory.Params>({ + name: 'create-model-from-trajectory', display: { - name: 'Structure from Model', + name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' }, - from: [SO.Models], - to: [SO.Structure], + from: [SO.Molecule.Trajectory], + to: [SO.Molecule.Model], params: { default: () => ({ modelIndex: 0 }), controls: a => ({ modelIndex: PD.Range('Model Index', 'Model Index', 0, 0, Math.max(0, a.data.length - 1), 1) }) @@ -63,61 +63,84 @@ const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Struc isApplicable: a => a.data.length > 0, apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); - let s = Structure.ofModel(a.data[params.modelIndex]); + const model = a.data[params.modelIndex]; + const label = { label: `Model ${model.modelNum}` }; + return new SO.Molecule.Model(model, label); + } +}); + +export { CreateStructure } +namespace CreateStructure { export interface Params { transform3d?: Mat4 } } +const CreateStructure = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, CreateStructure.Params>({ + name: 'create-structure-from-model', + display: { + name: 'Structure from Model', + description: 'Create a molecular structure from the specified model.' + }, + from: [SO.Molecule.Model], + to: [SO.Molecule.Structure], + apply({ a, params }) { + let s = Structure.ofModel(a.data); if (params.transform3d) s = Structure.transform(s, params.transform3d); - return new SO.Structure({ label: `Model ${s.models[0].modelNum}`, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); + const label = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }; + return new SO.Molecule.Structure(s, label); } }); +function structureDesc(s: Structure) { + return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`; +} export { CreateStructureAssembly } namespace CreateStructureAssembly { export interface Params { /** if not specified, use the 1st */ id?: string } } -const CreateStructureAssembly = PluginStateTransform.Create<SO.Structure, SO.Structure, CreateStructureAssembly.Params>({ +const CreateStructureAssembly = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, CreateStructureAssembly.Params>({ name: 'create-structure-assembly', display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' }, - from: [SO.Structure], - to: [SO.Structure], + from: [SO.Molecule.Model], + to: [SO.Molecule.Structure], params: { default: () => ({ id: void 0 }), controls(a) { - const { model } = a.data; + const model = a.data; const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]); return { id: PD.Select('Asm Id', 'Assembly Id', ids.length ? ids[0][0] : '', ids) }; } }, - isApplicable: a => a.data.models.length === 1 && a.data.model.symmetry.assemblies.length > 0, apply({ a, params }) { return Task.create('Build Assembly', async ctx => { let id = params.id; - const model = a.data.model; + const model = a.data; if (!id && model.symmetry.assemblies.length) id = model.symmetry.assemblies[0].id; - const asm = ModelSymmetry.findAssembly(a.data.model, id || ''); + const asm = ModelSymmetry.findAssembly(model, id || ''); if (!asm) throw new Error(`Assembly '${id}' not found`); - const s = await StructureSymmetry.buildAssembly(a.data, id!).runInContext(ctx); - return new SO.Structure({ label: `Assembly ${id}`, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); + const base = Structure.ofModel(model); + const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx); + const label = { label: `Assembly ${id}`, description: structureDesc(s) }; + return new SO.Molecule.Structure(s, label); }) } }); export { CreateStructureSelection } namespace CreateStructureSelection { export interface Params { query: Expression, label?: string } } -const CreateStructureSelection = PluginStateTransform.Create<SO.Structure, SO.Structure, CreateStructureSelection.Params>({ +const CreateStructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, CreateStructureSelection.Params>({ name: 'create-structure-selection', display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified model.' }, - from: [SO.Structure], - to: [SO.Structure], + from: [SO.Molecule.Structure], + to: [SO.Molecule.Structure], apply({ a, params }) { // TODO: use cache, add "update" const compiled = compile<StructureSelection>(params.query); const result = compiled(new QueryContext(a.data)); const s = StructureSelection.unionStructure(result); - return new SO.Structure({ label: `${params.label || 'Selection'}`, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); + const label = { label: `${params.label || 'Selection'}`, description: structureDesc(s) }; + return new SO.Molecule.Structure(s, label); } }); diff --git a/src/mol-plugin/state/transforms/visuals.ts b/src/mol-plugin/state/transforms/visuals.ts index dc7a49f265798d5d11476f06cc2591312e153ae2..3c76a56f5d2c3f49b30cdab808c6f6c44f40a2e3 100644 --- a/src/mol-plugin/state/transforms/visuals.ts +++ b/src/mol-plugin/state/transforms/visuals.ts @@ -6,10 +6,8 @@ import { Transformer } from 'mol-state'; import { Task } from 'mol-task'; -import { PluginStateTransform } from '../base'; -import { PluginStateObjects as SO } from '../objects'; -// import { CartoonRepresentation, DefaultCartoonProps } from 'mol-repr/structure/representation/cartoon'; -// import { BallAndStickRepresentation } from 'mol-repr/structure/representation/ball-and-stick'; +import { PluginStateTransform } from '../objects'; +import { PluginStateObject as SO } from '../objects'; import { PluginContext } from 'mol-plugin/context'; import { ColorTheme } from 'mol-theme/color'; import { SizeTheme } from 'mol-theme/size'; @@ -21,17 +19,16 @@ const representationRegistry = new RepresentationRegistry() export { CreateStructureRepresentation } namespace CreateStructureRepresentation { export interface Params { } } -const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, CreateStructureRepresentation.Params>({ +const CreateStructureRepresentation = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Representation3D, CreateStructureRepresentation.Params>({ name: 'create-structure-representation', display: { name: 'Create 3D Representation' }, - from: [SO.Structure], - to: [SO.StructureRepresentation3D], + from: [SO.Molecule.Structure], + to: [SO.Molecule.Representation3D], apply({ a, params }, plugin: PluginContext) { return Task.create('Structure Representation', async ctx => { const repr = representationRegistry.create('cartoon', { colorThemeRegistry, sizeThemeRegistry }, a.data) - // const repr = BallAndStickRepresentation(); // CartoonRepresentation(); await repr.createOrUpdate({ webgl: plugin.canvas3d.webgl, colorThemeRegistry, sizeThemeRegistry }, {}, {}, a.data).runInContext(ctx); - return new SO.StructureRepresentation3D({ label: 'Visual Repr.' }, repr); + return new SO.Molecule.Representation3D(repr); }); }, update({ a, b }, plugin: PluginContext) { diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx new file mode 100644 index 0000000000000000000000000000000000000000..db43db77315ab37271bdd5b436115d5176272961 --- /dev/null +++ b/src/mol-plugin/ui/base.tsx @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react'; +import { Observable, Subscription } from 'rxjs'; +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> { + static contextType = PluginReactContext; + readonly plugin: PluginContext; + + private subs: Subscription[] | undefined = void 0; + + protected subscribe<T>(obs: Observable<T>, action: (v: T) => void) { + if (typeof this.subs === 'undefined') this.subs = [] + this.subs.push(obs.subscribe(action)); + } + + componentWillUnmount() { + if (!this.subs) return; + for (const s of this.subs) s.unsubscribe(); + } + + protected init?(): void; + + constructor(props: P, context?: any) { + super(props, context); + this.plugin = context; + if (this.init) this.init(); + } +} + +export abstract class PurePluginComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> { + static contextType = PluginReactContext; + readonly plugin: PluginContext; + + private subs: Subscription[] | undefined = void 0; + + protected subscribe<T>(obs: Observable<T>, action: (v: T) => void) { + if (typeof this.subs === 'undefined') this.subs = [] + this.subs.push(obs.subscribe(action)); + } + + componentWillUnmount() { + if (!this.subs) return; + for (const s of this.subs) s.unsubscribe(); + } + + protected init?(): void; + + constructor(props: P, context?: any) { + super(props, context); + this.plugin = context; + if (this.init) this.init(); + } +} \ 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 eecfb2dd2366aa1105885c9119ebe1f2e1e64905..610b37c92f2b55f2c71a4045e494f47c30b41e83 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -5,87 +5,34 @@ */ import * as React from 'react'; -import { PluginContext } from '../context'; -import { Transform, Transformer, StateObject } from 'mol-state'; -import { ParametersComponent } from 'mol-app/component/parameters'; - -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(); - console.log(btoa(JSON.stringify(this._snap))); - } - private setSnapshot = () => { - if (!this._snap) return; - this.props.plugin.state.setSnapshot(this._snap); - } +import { PluginCommands } from 'mol-plugin/command'; +import { UpdateTrajectory } from 'mol-plugin/state/actions/basic'; +import { PluginComponent } from './base'; +export class Controls extends PluginComponent<{ }, { }> { 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/> - <hr /> - <button onClick={this.getSnapshot}>Get Snapshot</button> - <button onClick={this.setSnapshot}>Set Snapshot</button> - </div>; - } -} - -export class _test_CreateTransform extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, transformer: Transformer }, { params: any }> { - private getObj() { - const obj = this.props.plugin.state.data.objects.get(this.props.nodeRef)!; - return obj; - } - - private getDefaultParams() { - const p = this.props.transformer.definition.params; - if (!p || !p.default) return { }; - const obj = this.getObj(); - 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 obj = this.getObj(); - if (!obj.obj) return { }; - return p.controls(obj.obj, this.props.plugin); - } + return <> - private create() { - console.log(this.props.transformer.definition.name, this.state.params); - this.props.plugin._test_applyTransform(this.props.nodeRef, this.props.transformer, this.state.params); + </>; } +} - state = { params: this.getDefaultParams() } - +export class TrajectoryControls extends PluginComponent { render() { - const obj = this.getObj(); - if (obj.state !== StateObject.StateType.Ok) { - // TODO filter this elsewhere - return <div />; - } - - const t = this.props.transformer; - - 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> - <ParametersComponent params={this.getParamDef()} values={this.state.params as any} onChange={(k, v) => { - this.setState({ params: { ...this.state.params, [k]: v } }); - }} /> - <button onClick={() => this.create()} style={{ width: '100%' }}>Create</button> + return <div> + <b>Trajectory: </b> + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'advance', by: -1 }) + })}><<</button> + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'reset' }) + })}>Reset</button> + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'advance', by: +1 }) + })}>>></button><br /> </div> } } \ No newline at end of file diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c9071c0103ebe26ff2a99a78763329f1fada56f0 --- /dev/null +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import { ParamDefinition as PD } from 'mol-util/param-definition'; + +export interface ParameterControlsProps<P extends PD.Params = PD.Params> { + params: P, + values: any, + onChange: ParamOnChange, + isEnabled?: boolean, + onEnter?: () => void +} + +export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> { + render() { + const common = { + changes: this.props.onChange, + isEnabled: this.props.isEnabled, + onEnter: this.props.onEnter, + } + const params = this.props.params; + const values = this.props.values; + return <div style={{ width: '100%' }}> + {Object.keys(params).map(key => <ParamWrapper control={controlFor(params[key])} param={params[key]} key={key} {...common} name={key} value={values[key]} />)} + </div>; + } +} + +function controlFor(param: PD.Any): ValueControl { + switch (param.type) { + case 'boolean': return BoolControl; + case 'number': return NumberControl; + case 'range': return NumberControl; + case 'multi-select': throw new Error('nyi'); + case 'color': throw new Error('nyi'); + case 'select': return SelectControl; + case 'text': return TextControl; + } + throw new Error('not supporter'); +} + +type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, changes: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean } +export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void +type ValueControlProps<P extends PD.Base<any> = PD.Base<any>> = { value: any, param: P, isEnabled?: boolean, onChange: (v: any) => void, onEnter?: () => void } +type ValueControl = React.ComponentClass<ValueControlProps<any>> + +export class ParamWrapper extends React.PureComponent<ParamWrapperProps> { + onChange = (value: any) => { + this.props.changes({ param: this.props.param, name: this.props.name, value }); + } + + render() { + return <div> + <span title={this.props.param.description}>{this.props.param.label}</span> + <div> + <this.props.control value={this.props.value} param={this.props.param} onChange={this.onChange} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} /> + </div> + </div>; + } +} + +export class BoolControl extends React.PureComponent<ValueControlProps> { + onClick = () => { + this.props.onChange(!this.props.value); + } + + render() { + return <button onClick={this.onClick} disabled={!this.props.isEnabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>; + } +} + +export class NumberControl extends React.PureComponent<ValueControlProps<PD.Numeric>, { value: string }> { + // state = { value: this.props.value } + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.onChange(+e.target.value); + // this.setState({ value: e.target.value }); + } + + render() { + return <input type='range' + value={'' + this.props.value} + min={this.props.param.min} + max={this.props.param.max} + step={this.props.param.step} + onChange={this.onChange} + />; + } +} + +export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>> { + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + if (value !== this.props.value) { + this.props.onChange(value); + } + } + + onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (!this.props.onEnter) return; + if ((e.keyCode === 13 || e.charCode === 13)) { + this.props.onEnter(); + } + } + + render() { + return <input type='text' + value={this.props.value || ''} + onChange={this.onChange} + onKeyPress={this.props.onEnter ? this.onKeyPress : void 0} + />; + } +} + +export class SelectControl extends React.PureComponent<ValueControlProps<PD.Select<any>>> { + onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { + this.setState({ value: e.target.value }); + this.props.onChange(e.target.value); + } + + render() { + return <select value={this.props.value || ''} onChange={this.onChange}> + {this.props.param.options.map(([value, label]) => <option key={label} value={value}>{label}</option>)} + </select>; + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 9e1f84a230f7bf680ae368c8c79093060663f897..bd0435deb4ac2c9adee7715d999c607500e79dc3 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -7,50 +7,141 @@ import * as React from 'react'; import { PluginContext } from '../context'; import { StateTree } from './state-tree'; -import { Viewport } from './viewport'; -import { Controls, _test_CreateTransform } from './controls'; -import { Transformer } from 'mol-state'; +import { Viewport, ViewportControls } from './viewport'; +import { Controls, TrajectoryControls } from './controls'; +import { PluginComponent, PluginReactContext } from './base'; +import { CameraSnapshots } from './camera'; +import { StateSnapshots } from './state'; +import { List } from 'immutable'; +import { LogEntry } from 'mol-util/log-entry'; +import { formatTime } from 'mol-util'; +import { BackgroundTaskProgress } from './task'; +import { ApplyActionContol } from './state/apply-action'; +import { PluginState } from 'mol-plugin/state'; +import { UpdateTransformContol } from './state/update-transform'; -// TODO: base object with subscribe helpers - -export class Plugin extends React.Component<{ plugin: PluginContext }, { }> { +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' }}> - <StateTree plugin={this.props.plugin} /> - <hr /> - <_test_CurrentObject plugin={this.props.plugin} /> - </div> - <div style={{ position: 'absolute', left: '350px', right: '250px', height: '100%' }}> - <Viewport plugin={this.props.plugin} /> - </div> - <div style={{ position: 'absolute', width: '250px', right: '0', height: '100%' }}> - <Controls plugin={this.props.plugin} /> + return <PluginReactContext.Provider value={this.props.plugin}> + <div style={{ position: 'absolute', width: '100%', height: '100%', fontFamily: 'monospace' }}> + <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll', padding: '10px' }}> + <State /> + </div> + <div style={{ position: 'absolute', left: '350px', right: '300px', top: '0', bottom: '100px' }}> + <Viewport /> + <div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}> + <TrajectoryControls /> + </div> + <ViewportControls /> + <div style={{ position: 'absolute', left: '10px', bottom: '10px', color: 'white' }}> + <BackgroundTaskProgress /> + </div> + </div> + <div style={{ position: 'absolute', width: '300px', right: '0', top: '0', bottom: '0', padding: '10px', overflowY: 'scroll' }}> + <CurrentObject /> + <hr /> + <Controls /> + <hr /> + <CameraSnapshots /> + <hr /> + <StateSnapshots /> + </div> + <div style={{ position: 'absolute', right: '300px', left: '350px', bottom: '0', height: '100px', overflow: 'hidden' }}> + <Log /> + </div> </div> + </PluginReactContext.Provider>; + } +} + +export class State extends PluginComponent { + componentDidMount() { + this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate()); + } + + set(kind: PluginState.Kind) { + // TODO: do command for this? + this.plugin.state.setKind(kind); + } + + render() { + const kind = this.plugin.state.behavior.kind.value; + return <> + <button onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button> + <button onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button> + <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} /> + </> + } +} + +export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { + private wrapper = React.createRef<HTMLDivElement>(); + + componentDidMount() { + this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) })); + } + + componentDidUpdate() { + this.scrollToBottom(); + } + + state = { entries: List<LogEntry>() }; + + private scrollToBottom() { + const log = this.wrapper.current; + if (log) log.scrollTop = log.scrollHeight - log.clientHeight - 1; + } + + render() { + return <div ref={this.wrapper} style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', padding: '10px', overflowY: 'scroll' }}> + <ul style={{ listStyle: 'none' }}> + {this.state.entries.map((e, i) => <li key={i} style={{ borderBottom: '1px solid #999', padding: '3px' }}> + [{e!.type}] [{formatTime(e!.timestamp)}] {e!.message} + </li>)} + </ul> </div>; } } -export class _test_CurrentObject extends React.Component<{ plugin: PluginContext }, { }> { +export class CurrentObject extends PluginComponent { + get current() { + return this.plugin.state.behavior.currentObject.value; + } + componentDidMount() { - // TODO: move to constructor? - this.props.plugin.behaviors.state.data.currentObject.subscribe(() => this.forceUpdate()); + this.subscribe(this.plugin.state.behavior.currentObject, o => { + this.forceUpdate(); + }); + + this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { + const current = this.current; + if (current.ref !== ref || current.state !== state) return; + this.forceUpdate(); + }); } + render() { - const ref = this.props.plugin.behaviors.state.data.currentObject.value.ref; + const current = this.current; + + const ref = current.ref; // const n = this.props.plugin.state.data.tree.nodes.get(ref)!; - const obj = this.props.plugin.state.data.objects.get(ref)!; + const obj = current.state.cells.get(ref)!; const type = obj && obj.obj ? obj.obj.type : void 0; - const transforms = type - ? Transformer.fromType(type) + const transform = current.state.tree.transforms.get(ref); + + const actions = type + ? current.state.actions.fromType(type) : [] return <div> - Current Ref: {this.props.plugin.behaviors.state.data.currentObject.value.ref} <hr /> + <h3>{obj.obj ? obj.obj.label : ref}</h3> + <UpdateTransformContol state={current.state} transform={transform} /> + <hr /> + <h3>Create</h3> { - transforms.map((t, i) => <_test_CreateTransform key={`${t.id} ${ref} ${i}`} plugin={this.props.plugin} transformer={t} nodeRef={ref} />) + actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />) } </div>; } diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index f7dd468e2a24c82b03dc95b82d28dbfad96c379c..36455d8536dab7ab02048f1d5cd9efe3e7b14c0b 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -5,49 +5,148 @@ */ import * as React from 'react'; -import { PluginContext } from '../context'; -import { PluginStateObject } from 'mol-plugin/state/base'; -import { StateObject } from 'mol-state' +import { PluginStateObject } from 'mol-plugin/state/objects'; +import { State } from 'mol-state' import { PluginCommands } from 'mol-plugin/command'; +import { PluginComponent } from './base'; -export class StateTree extends React.Component<{ plugin: PluginContext }, { }> { +export class StateTree extends PluginComponent<{ state: State }, { }> { componentDidMount() { - // TODO: move to constructor? - this.props.plugin.events.state.data.updated.subscribe(() => this.forceUpdate()); + // this.subscribe(this.props.state.events.changed, () => { + // this.forceUpdate() + // }); } + render() { // const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!; - const n = this.props.plugin.state.data.tree.rootRef; + const n = this.props.state.tree.root.ref; return <div> - <StateTreeNode plugin={this.props.plugin} nodeRef={n} key={n} /> - { /* n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />) */} + <StateTreeNode state={this.props.state} nodeRef={n} /> + {/* n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />) */} </div>; } } -export class StateTreeNode extends React.Component<{ plugin: PluginContext, nodeRef: string }, { }> { +class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { }> { + is(e: State.ObjectEvent) { + return e.ref === this.props.nodeRef && e.state === this.props.state; + } + + get cellState() { + return this.props.state.tree.cellStates.get(this.props.nodeRef); + } + + componentDidMount() { + let isCollapsed = this.cellState.isCollapsed; + this.subscribe(this.plugin.events.state.cell.stateUpdated, e => { + if (this.is(e) && isCollapsed !== e.cellState.isCollapsed) { + isCollapsed = e.cellState.isCollapsed; + this.forceUpdate(); + } + }); + + this.subscribe(this.plugin.events.state.cell.created, e => { + if (this.props.state === e.state && this.props.nodeRef === e.cell.transform.parent) { + this.forceUpdate(); + } + }); + + this.subscribe(this.plugin.events.state.cell.removed, e => { + if (this.props.state === e.state && this.props.nodeRef === e.parent) { + this.forceUpdate(); + } + }); + } + render() { - const n = this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!; - const obj = this.props.plugin.state.data.objects.get(this.props.nodeRef)!; - if (!obj.obj) { - return <div style={{ borderLeft: '1px solid black', paddingLeft: '5px' }}> - {StateObject.StateType[obj.state]} {obj.errorText} - </div>; - } - const props = obj.obj!.props as PluginStateObject.Props; - const type = obj.obj!.type.info as PluginStateObject.TypeInfo; - return <div style={{ borderLeft: '1px solid #999', paddingLeft: '7px' }}> + const cellState = this.props.state.tree.cellStates.get(this.props.nodeRef); + + const expander = <> [<a href='#' onClick={e => { e.preventDefault(); - PluginCommands.Data.RemoveObject.dispatch(this.props.plugin, { ref: this.props.nodeRef }); - }}>X</a>][<span title={type.description}>{ type.shortName }</span>] <a href='#' onClick={e => { - e.preventDefault(); - PluginCommands.Data.SetCurrentObject.dispatch(this.props.plugin, { ref: this.props.nodeRef }); - }}>{props.label}</a> {props.description ? <small>{props.description}</small> : void 0} - {n.children.size === 0 + PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + }}>{cellState.isCollapsed ? '+' : '-'}</a>] + </>; + + const children = this.props.state.tree.children.get(this.props.nodeRef); + return <div> + {children.size === 0 ? void 0 : expander} <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} /> + {cellState.isCollapsed || children.size === 0 ? void 0 - : <div style={{ marginLeft: '3px' }}>{n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />)}</div> + : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999' }}>{children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}</div> } </div>; } +} + +class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State }> { + is(e: State.ObjectEvent) { + return e.ref === this.props.nodeRef && e.state === this.props.state; + } + + componentDidMount() { + this.subscribe(this.plugin.events.state.cell.stateUpdated, e => { + if (this.is(e)) this.forceUpdate(); + }); + + let isCurrent = this.is(this.props.state.behaviors.currentObject.value); + + this.subscribe(this.plugin.state.behavior.currentObject, e => { + let update = false; + if (this.is(e)) { + if (!isCurrent) { + isCurrent = true; + update = true; + } + } else if (isCurrent) { + isCurrent = false; + update = true; + } + if (update && e.state.tree.transforms.has(this.props.nodeRef)) { + this.forceUpdate(); + } + }); + } + + render() { + const n = this.props.state.tree.transforms.get(this.props.nodeRef)!; + const cell = this.props.state.cells.get(this.props.nodeRef)!; + + const isCurrent = this.is(this.props.state.behaviors.currentObject.value); + + const remove = <>[<a href='#' onClick={e => { + e.preventDefault(); + PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + }}>X</a>]</> + + let label: any; + if (cell.status !== 'ok' || !cell.obj) { + const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name; + label = <><b>{cell.status}</b> <a href='#' onClick={e => { + e.preventDefault(); + PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + }}>{name}</a>: <i>{cell.errorText}</i></>; + } else { + const obj = cell.obj as PluginStateObject.Any; + label = <><a href='#' onClick={e => { + e.preventDefault(); + PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + }}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>; + } + + const cellState = this.props.state.tree.cellStates.get(this.props.nodeRef); + + if (!cellState) console.log('missing state', this.props.nodeRef, this.props.state.tree, this.props.state.tree.transforms.has(this.props.nodeRef)); + + const visibility = <> + [<a href='#' onClick={e => { + e.preventDefault(); + PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + }}>{cellState.isHidden ? 'H' : 'V'}</a>] + </>; + + return <> + {remove}{visibility} {isCurrent ? <b>{label}</b> : label} + </>; + } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx new file mode 100644 index 0000000000000000000000000000000000000000..10ce5b866bf2d4989123cf656939370273997a2b --- /dev/null +++ b/src/mol-plugin/ui/state.tsx @@ -0,0 +1,129 @@ +/** + * 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'; +import { shallowEqual } from 'mol-util'; +import { List } from 'immutable'; +import { LogEntry } from 'mol-util/log-entry'; + +export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> { + state = { serverUrl: 'http://webchem.ncbr.muni.cz/molstar-state' } + + updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) }; + + render() { + return <div> + <h3>State Snapshots</h3> + <StateSnapshotControls serverUrl={this.state.serverUrl} serverChanged={this.updateServerUrl} /> + <b>Local</b> + <LocalStateSnapshotList /> + <RemoteStateSnapshotList serverUrl={this.state.serverUrl} /> + </div>; + } +} + +class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { + state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false }; + + add = () => { + PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.name, description: this.state.description }); + this.setState({ name: '', description: '' }) + } + + clear = () => { + PluginCommands.State.Snapshots.Clear.dispatch(this.plugin, {}); + } + + shouldComponentUpdate(nextProps: { serverUrl: string, serverChanged: (url: string) => void }, nextState: { name: string, description: string, serverUrl: string, isUploading: boolean }) { + return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); + } + + upload = async () => { + this.setState({ isUploading: true }); + await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl }); + this.setState({ isUploading: false }); + } + + 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 })} /> + <input type='text' value={this.state.serverUrl} placeholder='Server URL...' style={{ width: '100%', display: 'block' }} onChange={e => { + this.setState({ serverUrl: e.target.value }); + this.props.serverChanged(e.target.value); + }} /> + <button style={{ float: 'right' }} onClick={this.clear}>Clear</button> + <button onClick={this.add}>Add Local</button> + <button onClick={this.upload} disabled={this.state.isUploading}>Upload</button> + </div>; + } +} + +class LocalStateSnapshotList 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>; + } +} + +type RemoteEntry = { url: string, timestamp: number, id: string, name: string, description: string } +class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { + state = { entries: List<RemoteEntry>(), isFetching: false }; + + componentDidMount() { + this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); + this.refresh(); + } + + refresh = async () => { + try { + this.setState({ isFetching: true }); + const req = await fetch(`${this.props.serverUrl}/list`); + const json: RemoteEntry[] = await req.json(); + this.setState({ entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({ ...e, url: `${this.props.serverUrl}/get/${e.id}` }))), isFetching: false }) + } catch (e) { + this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e)); + this.setState({ entries: List<RemoteEntry>(), isFetching: false }) + } + } + + fetch(url: string) { + return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url }); + } + + render() { + return <div> + <b>Remote</b> <button onClick={this.refresh} disabled={this.state.isFetching}>Refresh</button> + <ul style={{ listStyle: 'none' }}> + {this.state.entries.valueSeq().map(e =><li key={e!.id}> + <button onClick={this.fetch(e!.url)} disabled={this.state.isFetching}>Fetch</button> + {e!.name} <small>{e!.description}</small> + </li>)} + </ul> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4e34aced24449a36c41371f2cd34f17ce6c761ba --- /dev/null +++ b/src/mol-plugin/ui/state/apply-action.tsx @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react'; +import { PluginCommands } from 'mol-plugin/command'; +import { State, Transform } from 'mol-state'; +import { StateAction } from 'mol-state/action'; +import { Subject } from 'rxjs'; +import { PurePluginComponent } from '../base'; +import { StateTransformParameters } from './parameters'; +import { memoizeOne } from 'mol-util/memoize'; +import { PluginContext } from 'mol-plugin/context'; + +export { ApplyActionContol }; + +namespace ApplyActionContol { + export interface Props { + plugin: PluginContext, + nodeRef: Transform.Ref, + state: State, + action: StateAction + } + + export interface ComponentState { + nodeRef: Transform.Ref, + params: any, + error?: string, + busy: boolean, + isInitial: boolean + } +} + +class ApplyActionContol extends PurePluginComponent<ApplyActionContol.Props, ApplyActionContol.ComponentState> { + private busy: Subject<boolean>; + + onEnter = () => { + if (this.state.error) return; + this.apply(); + } + + source = this.props.state.cells.get(this.props.nodeRef)!.obj!; + + getInfo = memoizeOne((t: Transform.Ref) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef)); + + events: StateTransformParameters.Props['events'] = { + onEnter: this.onEnter, + onChange: (params, isInitial, errors) => { + this.setState({ params, isInitial, error: errors && errors[0] }) + } + } + + // getInitialParams() { + // const p = this.props.action.definition.params; + // if (!p || !p.default) return {}; + // return p.default(this.source, this.plugin); + // } + + // initialErrors() { + // const p = this.props.action.definition.params; + // if (!p || !p.validate) return void 0; + // const errors = p.validate(this.info.initialValues, this.source, this.plugin); + // return errors && errors[0]; + // } + + state = { nodeRef: this.props.nodeRef, error: void 0, isInitial: true, params: this.getInfo(this.props.nodeRef).initialValues, busy: false }; + + apply = async () => { + this.setState({ busy: true }); + + try { + await PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.props.state, + action: this.props.action.create(this.state.params), + ref: this.props.nodeRef + }); + } finally { + this.busy.next(false); + } + } + + init() { + this.busy = new Subject(); + this.subscribe(this.busy, busy => this.setState({ busy })); + } + + refresh = () => { + this.setState({ params: this.getInfo(this.props.nodeRef).initialValues, isInitial: true, error: void 0 }); + } + + static getDerivedStateFromProps(props: ApplyActionContol.Props, state: ApplyActionContol.ComponentState) { + if (props.nodeRef === state.nodeRef) return null; + const source = props.state.cells.get(props.nodeRef)!.obj!; + const definition = props.action.definition.params || { }; + const initialValues = definition.default ? definition.default(source, props.plugin) : {}; + + const newState: Partial<ApplyActionContol.ComponentState> = { + nodeRef: props.nodeRef, + params: initialValues, + isInitial: true, + error: void 0 + }; + return newState; + } + + render() { + const info = this.getInfo(this.props.nodeRef); + const action = this.props.action; + + return <div> + <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div> + + <StateTransformParameters info={info} events={this.events} params={this.state.params} isEnabled={!this.state.busy} /> + + <div style={{ textAlign: 'right' }}> + <span style={{ color: 'red' }}>{this.state.error}</span> + {this.state.isInitial ? void 0 : <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}>↻</button>} + <button onClick={this.apply} disabled={!!this.state.error || this.state.busy}>Create</button> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/parameters.tsx b/src/mol-plugin/ui/state/parameters.tsx new file mode 100644 index 0000000000000000000000000000000000000000..804770f15682002942091539a8ecc29ff60d8412 --- /dev/null +++ b/src/mol-plugin/ui/state/parameters.tsx @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateObject, Transformer, State, Transform, StateObjectCell } from 'mol-state'; +import { shallowEqual } from 'mol-util/object'; +import * as React from 'react'; +import { PurePluginComponent } from '../base'; +import { ParameterControls, ParamOnChange } from '../controls/parameters'; +import { StateAction } from 'mol-state/action'; +import { PluginContext } from 'mol-plugin/context'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; + +export { StateTransformParameters }; + +class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> { + getDefinition() { + const controls = this.props.info.definition.controls; + if (!controls) return { }; + return controls!(this.props.info.source, this.plugin) + } + + validate(params: any) { + const validate = this.props.info.definition.validate; + if (!validate) return void 0; + return validate(params, this.props.info.source, this.plugin) + } + + areInitial(params: any) { + const areEqual = this.props.info.definition.areEqual; + if (!areEqual) return shallowEqual(params, this.props.info.initialValues); + return areEqual(params, this.props.info.initialValues); + } + + onChange: ParamOnChange = ({ name, value }) => { + const params = { ...this.props.params, [name]: value }; + this.props.events.onChange(params, this.areInitial(params), this.validate(params)); + }; + + render() { + return <ParameterControls params={this.props.info.params} values={this.props.params} onChange={this.onChange} onEnter={this.props.events.onEnter} isEnabled={this.props.isEnabled} />; + } +} + + +namespace StateTransformParameters { + export interface Props { + info: { + definition: Transformer.ParamsDefinition, + params: PD.Params, + initialValues: any, + source: StateObject, + isEmpty: boolean + }, + events: { + onChange: (params: any, areInitial: boolean, errors?: string[]) => void, + onEnter: () => void, + } + params: any, + isEnabled?: boolean + } + + export type Class = React.ComponentClass<Props> + + export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: Transform.Ref): Props['info'] { + const source = state.cells.get(nodeRef)!.obj!; + const definition = action.definition.params || { }; + const initialValues = definition.default ? definition.default(source, plugin) : {}; + const params = definition.controls ? definition.controls(source, plugin) : {}; + return { + source, + definition: action.definition.params || { }, + initialValues, + params, + isEmpty: Object.keys(params).length === 0 + }; + } + + export function infoFromTransform(plugin: PluginContext, state: State, transform: Transform): Props['info'] { + const cell = state.cells.get(transform.ref)!; + const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0; + const definition = transform.transformer.definition.params || { }; + const params = definition.controls ? definition.controls((source && source.obj) as any, plugin) : {}; + return { + source: (source && source.obj) as any, + definition, + initialValues: transform.params, + params, + isEmpty: Object.keys(params).length === 0 + } + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx new file mode 100644 index 0000000000000000000000000000000000000000..591e1f7556c60d52f93f81e3b7a5c06c67f2a9ab --- /dev/null +++ b/src/mol-plugin/ui/state/update-transform.tsx @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { State, Transform } from 'mol-state'; +import * as React from 'react'; +import { Subject } from 'rxjs'; +import { PurePluginComponent } from '../base'; +import { StateTransformParameters } from './parameters'; +import { memoizeOne } from 'mol-util/memoize'; + +export { UpdateTransformContol }; + +namespace UpdateTransformContol { + export interface Props { + transform: Transform, + state: State + } + + export interface ComponentState { + transform: Transform, + params: any, + error?: string, + busy: boolean, + isInitial: boolean + } +} + +class UpdateTransformContol extends PurePluginComponent<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> { + private busy: Subject<boolean>; + + onEnter = () => { + if (this.state.error) return; + this.apply(); + } + + getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform)); + + events: StateTransformParameters.Props['events'] = { + onEnter: this.onEnter, + onChange: (params, isInitial, errors) => { + this.setState({ params, isInitial, error: errors && errors[0] }) + } + } + + state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo(this.props.transform).initialValues, busy: false }; + + apply = async () => { + this.setState({ busy: true }); + + try { + await this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); + } finally { + this.busy.next(false); + } + } + + init() { + this.busy = new Subject(); + this.subscribe(this.busy, busy => this.setState({ busy })); + } + + refresh = () => { + this.setState({ params: this.props.transform.params, isInitial: true, error: void 0 }); + } + + static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) { + if (props.transform === state.transform) return null; + const newState: Partial<UpdateTransformContol.ComponentState> = { + transform: props.transform, + params: props.transform.params, + isInitial: true, + error: void 0 + }; + return newState; + } + + render() { + const info = this.getInfo(this.props.transform); + if (info.isEmpty) return <div>Nothing to update</div>; + + const tr = this.props.transform.transformer; + + return <div> + <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.id}</h3></div> + + <StateTransformParameters info={info} events={this.events} params={this.state.params} isEnabled={!this.state.busy} /> + + <div style={{ textAlign: 'right' }}> + <span style={{ color: 'red' }}>{this.state.error}</span> + {this.state.isInitial ? void 0 : <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}>↻</button>} + <button onClick={this.apply} disabled={!!this.state.error || this.state.busy || this.state.isInitial}>Update</button> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/task.tsx b/src/mol-plugin/ui/task.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c45b783ea05824070df202b5e24712b37cf61c09 --- /dev/null +++ b/src/mol-plugin/ui/task.tsx @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react'; +import { PluginComponent } 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> }> { + 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) }) + }); + this.subscribe(this.plugin.events.task.finished, ({ id }) => { + this.setState({ tracked: this.state.tracked.delete(id) }) + }) + } + + state = { tracked: OrderedMap<number, TaskManager.ProgressEvent>() }; + + render() { + return <div> + {this.state.tracked.valueSeq().map(e => <ProgressEntry key={e!.id} event={e!} />)} + </div>; + } +} + +class ProgressEntry extends PluginComponent<{ event: TaskManager.ProgressEvent }> { + render() { + const root = this.props.event.progress.root; + const subtaskCount = countSubtasks(this.props.event.progress.root) - 1; + const pr = root.progress.isIndeterminate + ? void 0 + : <>[{root.progress.current}/{root.progress.max}]</>; + const subtasks = subtaskCount > 0 + ? <>[{subtaskCount} subtask(s)]</> + : void 0 + return <div> + {root.progress.message} {pr} {subtasks} + </div>; + } +} + +function countSubtasks(progress: Progress.Node) { + if (progress.children.length === 0) return 1; + let sum = 0; + for (const c of progress.children) sum += countSubtasks(c); + return sum; +} \ No newline at end of file diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index a7ace28f1417e30a7faaf7273b83305f23c859ec..2ce8f7bb6319cdfa31364b12c0d33a976bad3f98 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -6,20 +6,28 @@ */ import * as React from 'react'; -import { PluginContext } from '../context'; -import { Loci, EmptyLoci, areLociEqual } from 'mol-model/loci'; -import { MarkerAction } from 'mol-geo/geometry/marker-data'; import { ButtonsType } from 'mol-util/input/input-observer'; - -interface ViewportProps { - plugin: PluginContext -} +import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify'; +import { PluginComponent } from './base'; +import { PluginCommands } from 'mol-plugin/command'; interface ViewportState { noWebGl: boolean } -export class Viewport extends React.Component<ViewportProps, ViewportState> { +export class ViewportControls extends PluginComponent { + resetCamera = () => { + PluginCommands.Camera.Reset.dispatch(this.plugin, {}); + } + + render() { + return <div style={{ position: 'absolute', right: '10px', top: '10px', height: '100%', color: 'white' }}> + <button onClick={this.resetCamera}>Reset Camera</button> + </div> + } +} + +export class Viewport extends PluginComponent<{ }, ViewportState> { private container: HTMLDivElement | null = null; private canvas: HTMLCanvasElement | null = null; @@ -27,42 +35,34 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> { noWebGl: false }; - handleResize() { - this.props.plugin.canvas3d.handleResize(); + private handleResize = () => { + this.plugin.canvas3d.handleResize(); } componentDidMount() { - if (!this.canvas || !this.container || !this.props.plugin.initViewer(this.canvas, this.container)) { + if (!this.canvas || !this.container || !this.plugin.initViewer(this.canvas, this.container)) { this.setState({ noWebGl: true }); } this.handleResize(); - const canvas3d = this.props.plugin.canvas3d; - canvas3d.input.resize.subscribe(() => this.handleResize()); - - let prevLoci: Loci = EmptyLoci; - canvas3d.input.move.subscribe(async ({x, y, inside, buttons}) => { - if (!inside || buttons) return; - const p = await canvas3d.identify(x, y); - if (p) { - const { loci } = canvas3d.getLoci(p); - - if (!areLociEqual(loci, prevLoci)) { - canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight); - canvas3d.mark(loci, MarkerAction.Highlight); - prevLoci = loci; - } - } - }) - - canvas3d.input.click.subscribe(async ({x, y, buttons}) => { - if (buttons !== ButtonsType.Flag.Primary) return - const p = await canvas3d.identify(x, y) - if (p) { - const { loci } = canvas3d.getLoci(p) - canvas3d.mark(loci, MarkerAction.Toggle) - } - }) + const canvas3d = this.plugin.canvas3d; + this.subscribe(canvas3d.input.resize, this.handleResize); + + const idHelper = new Canvas3dIdentifyHelper(this.plugin, 15); + + this.subscribe(canvas3d.input.move, ({x, y, inside, buttons}) => { + if (!inside || buttons) { return; } + idHelper.move(x, y); + }); + + this.subscribe(canvas3d.input.leave, () => { + idHelper.leave(); + }); + + this.subscribe(canvas3d.input.click, ({x, y, buttons}) => { + if (buttons !== ButtonsType.Flag.Primary) return; + idHelper.select(x, y); + }); } componentWillUnmount() { diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2cf8c442edc73cfc87cd3f1b0211d2158664d9c --- /dev/null +++ b/src/mol-plugin/util/canvas3d-identify.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginContext } from '../context'; +import { PickingId } from 'mol-geo/geometry/picking'; +import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci'; +import { Representation } from 'mol-repr/representation'; + +export class Canvas3dIdentifyHelper { + private cX = -1; + private cY = -1; + + private lastX = -1; + private lastY = -1; + + private id: PickingId | undefined = void 0; + + private currentIdentifyT = 0; + + private prevLoci: { loci: Loci, repr?: Representation.Any } = { loci: EmptyLoci }; + private prevT = 0; + + private inside = false; + + private async identify(select: boolean, t: number) { + if (this.lastX !== this.cX && this.lastY !== this.cY) { + this.id = await this.ctx.canvas3d.identify(this.cX, this.cY); + this.lastX = this.cX; + this.lastY = this.cY; + } + + if (!this.id) return; + + if (select) { + this.ctx.behaviors.canvas.selectLoci.next(this.ctx.canvas3d.getLoci(this.id)); + return; + } + + // only highlight the latest + if (!this.inside || this.currentIdentifyT !== t) { + return; + } + + const loci = this.ctx.canvas3d.getLoci(this.id); + if (loci.repr !== this.prevLoci.repr || !areLociEqual(loci.loci, this.prevLoci.loci)) { + this.ctx.behaviors.canvas.highlightLoci.next(loci); + this.prevLoci = loci; + } + } + + private animate: (t: number) => void = t => { + if (this.inside && t - this.prevT > 1000 / this.maxFps) { + this.prevT = t; + this.currentIdentifyT = t; + this.identify(false, t); + } + requestAnimationFrame(this.animate); + } + + leave() { + this.inside = false; + if (this.prevLoci.loci !== EmptyLoci) { + this.prevLoci = { loci: EmptyLoci }; + this.ctx.behaviors.canvas.highlightLoci.next(this.prevLoci); + this.ctx.canvas3d.requestDraw(true); + } + } + + move(x: number, y: number) { + this.inside = true; + this.cX = x; + this.cY = y; + } + + select(x: number, y: number) { + this.cX = x; + this.cY = y; + this.identify(true, 0); + } + + constructor(private ctx: PluginContext, private maxFps: number = 15) { + this.animate(0); + } +} \ No newline at end of file diff --git a/src/mol-plugin/util/logger.ts b/src/mol-plugin/util/logger.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/mol-plugin/util/task-manager.ts b/src/mol-plugin/util/task-manager.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c5cbb47e72aa442378ceae3bdd8df06d1193a30 --- /dev/null +++ b/src/mol-plugin/util/task-manager.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Task, Progress } from 'mol-task'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; +import { now } from 'mol-util/now'; + +export { TaskManager } + +class TaskManager { + private ev = RxEventHelper.create(); + private id = 0; + + readonly events = { + progress: this.ev<TaskManager.ProgressEvent>(), + finished: this.ev<{ id: number }>() + }; + + private track(id: number) { + return (progress: Progress) => { + const elapsed = now() - progress.root.progress.startedTime; + progress.root.progress.startedTime + this.events.progress.next({ + id, + level: elapsed < 250 ? 'none' : elapsed < 1500 ? 'background' : 'overlay', + progress + }); + }; + } + + async run<T>(task: Task<T>): Promise<T> { + const id = this.id++; + try { + const ret = await task.run(this.track(id), 100); + return ret; + } finally { + this.events.finished.next({ id }); + } + } + + dispose() { + this.ev.dispose(); + } +} + +namespace TaskManager { + export type ReportLevel = 'none' | 'background' | 'overlay' + + export interface ProgressEvent { + id: number, + level: ReportLevel, + progress: Progress + } + + function delay(time: number): Promise<void> { + return new Promise(res => setTimeout(res, time)); + } + export function testTask(N: number) { + return Task.create('Test', async ctx => { + let i = 0; + while (i < N) { + await delay(100 + Math.random() * 200); + if (ctx.shouldUpdate) { + await ctx.update({ message: 'Step ' + i, current: i, max: N, isIndeterminate: false }); + } + i++; + } + }) + } +} \ No newline at end of file diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad57879c7781e64c6d8993451a01cc93c59e6565 --- /dev/null +++ b/src/mol-state/action.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Task } from 'mol-task'; +import { UUID } from 'mol-util'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { StateObject, StateObjectCell } from './object'; +import { State } from './state'; +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> +} + +namespace StateAction { + export type Id = string & { '@type': 'transformer-id' } + export type Params<T extends StateAction<any, any, any>> = T extends StateAction<any, any, infer P> ? P : unknown; + 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, + state: State, + params: P + } + + export interface Definition<A extends StateObject = StateObject, T = any, P = unknown> { + readonly from: StateObject.Ctor[], + readonly display?: { readonly name: string, readonly description?: string }, + + /** + * Apply an action that modifies the State specified in Params. + */ + apply(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>, + + readonly params?: Transformer<A, any, P>['definition']['params'], + + /** Test if the transform can be applied to a given node */ + isApplicable?(a: A, globalCtx: unknown): boolean + } + + export function create<A extends StateObject, T, P>(definition: Definition<A, T, P>): StateAction<A, T, P> { + const action: StateAction<A, T, P> = { + create(params) { return { action, params }; }, + id: UUID.create22(), + definition + }; + return action; + } + + export function fromTransformer<T extends Transformer>(transformer: T) { + const def = transformer.definition; + return create<Transformer.From<T>, void, Transformer.Params<T>>({ + from: def.from, + display: def.display, + params: def.params as Transformer<Transformer.From<T>, any, Transformer.Params<T>>['definition']['params'], + apply({ cell, state, params }) { + const tree = state.build().to(cell.transform.ref).apply(transformer, params); + return state.update(tree); + } + }) + } +} \ No newline at end of file diff --git a/src/mol-state/action/manager.ts b/src/mol-state/action/manager.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e489df48ccf19d884881f97fd7152a730062276 --- /dev/null +++ b/src/mol-state/action/manager.ts @@ -0,0 +1,38 @@ +/** + * 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'; +import { Transformer } from 'mol-state/transformer'; + +export { StateActionManager } + +class StateActionManager { + private actions: Map<StateAction['id'], StateAction> = new Map(); + private fromTypeIndex = new Map<StateObject.Type, StateAction[]>(); + + add(actionOrTransformer: StateAction | Transformer) { + const action = Transformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer; + + 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/context.ts b/src/mol-state/context.ts deleted file mode 100644 index ff471db4eaa6786100ec01ddbbdf5dd3843ab0e7..0000000000000000000000000000000000000000 --- a/src/mol-state/context.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { StateObject } from './object'; -import { Transform } from './transform'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; - -export { StateContext } - -class StateContext { - private ev = RxEventHelper.create(); - - readonly events = { - object: { - stateChanged: this.ev<{ ref: Transform.Ref }>(), - propsChanged: this.ev<{ ref: Transform.Ref, newProps: unknown }>(), - - updated: this.ev<{ ref: Transform.Ref, obj?: StateObject }>(), - replaced: this.ev<{ ref: Transform.Ref, oldObj?: StateObject, newObj?: StateObject }>(), - created: this.ev<{ ref: Transform.Ref, obj: StateObject }>(), - removed: this.ev<{ ref: Transform.Ref, obj?: StateObject }>(), - - currentChanged: this.ev<{ ref: Transform.Ref }>() - }, - warn: this.ev<string>(), - updated: this.ev<void>() - }; - - readonly behaviors = { - currentObject: this.ev.behavior<{ ref: Transform.Ref }>(void 0 as any) - }; - - readonly globalContext: unknown; - readonly defaultObjectProps: unknown; - - dispose() { - this.ev.dispose(); - } - - constructor(params: { globalContext: unknown, defaultObjectProps: unknown, rootRef: Transform.Ref }) { - this.globalContext = params.globalContext; - this.defaultObjectProps = params.defaultObjectProps; - this.behaviors.currentObject.next({ ref: params.rootRef }); - } -} \ No newline at end of file diff --git a/src/mol-state/index.ts b/src/mol-state/index.ts index 89b5ea1a56aad8fe5352bfeb50649b20119bc8b5..8ef37d2fd8463422cf850a1f5aafb69b448ac16c 100644 --- a/src/mol-state/index.ts +++ b/src/mol-state/index.ts @@ -8,6 +8,4 @@ export * from './object' export * from './state' export * from './transformer' export * from './tree' -export * from './context' -export * from './transform' -export * from './selection' \ No newline at end of file +export * from './transform' \ No newline at end of file diff --git a/src/mol-plugin/state/action.ts b/src/mol-state/manager.ts similarity index 65% rename from src/mol-plugin/state/action.ts rename to src/mol-state/manager.ts index 79be247dea90d8ee04439378c5ac1b6280a20145..0042b15a93f4184628d575672d2570f63ac64dc6 100644 --- a/src/mol-plugin/state/action.ts +++ b/src/mol-state/manager.ts @@ -4,4 +4,4 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -// TODO actions that modify state and can be "applied" to certain state objects. \ No newline at end of file +// TODO manage snapshots etc \ No newline at end of file diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index dddfcb68b41229e7a54a39d3bbe0a4479d6fe887..97a2b9e4870dfd053303c6f6998587c799959a7c 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -1,60 +1,77 @@ - /** * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ -import { Transform } from './transform'; import { UUID } from 'mol-util'; +import { Transform } from './transform'; + +export { StateObject, StateObjectCell } -/** A mutable state object */ -export interface StateObject<P = any, D = any> { +interface StateObject<D = any, T extends StateObject.Type = { name: string, typeClass: any }> { readonly id: UUID, - readonly type: StateObject.Type, - readonly props: P, - readonly data: D + readonly type: T, + readonly data: D, + readonly label: string, + readonly description?: string, } -export namespace StateObject { - export enum StateType { - // The object has been successfully created - Ok, - // An error occured during the creation of the object - Error, - // The object is queued to be created - Pending, - // The object is currently being created - Processing +namespace StateObject { + export function factory<T extends Type>() { + return <D = { }>(type: T) => create<D, T>(type); } - export interface Type<Info = any> { - info: Info + export type Type<Cls extends string = string> = { name: string, typeClass: Cls } + export type Ctor = { new(...args: any[]): StateObject, type: any } + + export function create<Data, T extends Type>(type: T) { + return class implements StateObject<Data, T> { + static type = type; + static is(obj?: StateObject): obj is StateObject<Data, T> { return !!obj && type === obj.type; } + id = UUID.create22(); + type = type; + label: string; + description?: string; + constructor(public data: Data, props?: { label: string, description?: string }) { + this.label = props && props.label || type.name; + this.description = props && props.description; + } + } } +} - export function factory<TypeInfo, CommonProps>() { - return <D = { }, P = {}>(typeInfo: TypeInfo) => create<P & CommonProps, D, TypeInfo>(typeInfo); +interface StateObjectCell { + transform: Transform, + + // Which object was used as a parent to create data in this cell + sourceRef: Transform.Ref | undefined, + + version: string + status: StateObjectCell.Status, + + errorText?: string, + obj?: StateObject +} + +namespace StateObjectCell { + export type Status = 'ok' | 'error' | 'pending' | 'processing' + + export interface State { + isHidden: boolean, + isCollapsed: boolean } - export type Ctor = { new(...args: any[]): StateObject, type: Type } + export const DefaultState: State = { isHidden: false, isCollapsed: false }; - export function create<Props, Data, TypeInfo>(typeInfo: TypeInfo) { - const dataType: Type<TypeInfo> = { info: typeInfo }; - return class implements StateObject<Props, Data> { - static type = dataType; - static is(obj?: StateObject): obj is StateObject<Props, Data> { return !!obj && dataType === obj.type; } - id = UUID.create(); - type = dataType; - constructor(public props: Props, public data: Data) { } - } + export function areStatesEqual(a: State, b: State) { + return a.isHidden !== b.isHidden || a.isCollapsed !== b.isCollapsed; } - export interface Node { - ref: Transform.Ref, - state: StateType, - props: unknown, - errorText?: string, - obj?: StateObject, - version: string + export function isStateChange(a: State, b?: Partial<State>) { + if (!b) return false; + if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true; + if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true; + return false; } } \ No newline at end of file diff --git a/src/mol-state/selection.ts b/src/mol-state/selection.ts deleted file mode 100644 index 60c512855f7ab9070b5f25d93b43b70594464545..0000000000000000000000000000000000000000 --- a/src/mol-state/selection.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { StateObject } from './object'; -import { State } from './state'; -import { ImmutableTree } from './util/immutable-tree'; - -namespace StateSelection { - export type Selector = Query | Builder | string | StateObject.Node; - export type NodeSeq = StateObject.Node[] - export type Query = (state: State) => NodeSeq; - - export function select(s: Selector, state: State) { - return compile(s)(state); - } - - export function compile(s: Selector): Query { - const selector = s ? s : root(); - let query: Query; - if (isBuilder(selector)) query = (selector as any).compile(); - else if (isObj(selector)) query = (byValue(selector) as any).compile(); - else if (isQuery(selector)) query = selector; - else query = (byRef(selector as string) as any).compile(); - return query; - } - - function isObj(arg: any): arg is StateObject.Node { - return (arg as StateObject.Node).version !== void 0; - } - - function isBuilder(arg: any): arg is Builder { - return arg.compile !== void 0; - } - - function isQuery(arg: any): arg is Query { - return typeof arg === 'function'; - } - - export interface Builder { - flatMap(f: (n: StateObject.Node) => StateObject.Node[]): Builder; - mapEntity(f: (n: StateObject.Node) => StateObject.Node): Builder; - unique(): Builder; - - parent(): Builder; - first(): Builder; - filter(p: (n: StateObject.Node) => boolean): Builder; - subtree(): Builder; - children(): Builder; - ofType(t: StateObject.Type): Builder; - ancestorOfType(t: StateObject.Type): Builder; - } - - const BuilderPrototype: any = {}; - - function registerModifier(name: string, f: Function) { - BuilderPrototype[name] = function (this: any, ...args: any[]) { return f.call(void 0, this, ...args) }; - } - - function build(compile: () => Query): Builder { - return Object.create(BuilderPrototype, { compile: { writable: false, configurable: false, value: compile } }); - } - - export function root() { return build(() => (state: State) => [state.objects.get(state.tree.rootRef)!]) } - - - export function byRef(...refs: string[]) { - return build(() => (state: State) => { - const ret: StateObject.Node[] = []; - for (const ref of refs) { - const n = state.objects.get(ref); - if (!n) continue; - ret.push(n); - } - return ret; - }); - } - - export function byValue(...objects: StateObject.Node[]) { return build(() => (state: State) => objects); } - - registerModifier('flatMap', flatMap); - export function flatMap(b: Selector, f: (obj: StateObject.Node, state: State) => NodeSeq) { - const q = compile(b); - return build(() => (state: State) => { - const ret: StateObject.Node[] = []; - for (const n of q(state)) { - for (const m of f(n, state)) { - ret.push(m); - } - } - return ret; - }); - } - - registerModifier('mapEntity', mapEntity); - export function mapEntity(b: Selector, f: (n: StateObject.Node, state: State) => StateObject.Node | undefined) { - const q = compile(b); - return build(() => (state: State) => { - const ret: StateObject.Node[] = []; - for (const n of q(state)) { - const x = f(n, state); - if (x) ret.push(x); - } - return ret; - }); - } - - registerModifier('unique', unique); - export function unique(b: Selector) { - const q = compile(b); - return build(() => (state: State) => { - const set = new Set<string>(); - const ret: StateObject.Node[] = []; - for (const n of q(state)) { - if (!set.has(n.ref)) { - set.add(n.ref); - ret.push(n); - } - } - return ret; - }) - } - - registerModifier('first', first); - export function first(b: Selector) { - const q = compile(b); - return build(() => (state: State) => { - const r = q(state); - return r.length ? [r[0]] : []; - }); - } - - registerModifier('filter', filter); - export function filter(b: Selector, p: (n: StateObject.Node) => boolean) { return flatMap(b, n => p(n) ? [n] : []); } - - registerModifier('subtree', subtree); - export function subtree(b: Selector) { - return flatMap(b, (n, s) => { - const nodes = [] as string[]; - ImmutableTree.doPreOrder(s.tree, s.tree.nodes.get(n.ref), nodes, (x, _, ctx) => { ctx.push(x.ref) }); - return nodes.map(x => s.objects.get(x)!); - }); - } - - registerModifier('children', children); - export function children(b: Selector) { - return flatMap(b, (n, s) => { - const nodes: StateObject.Node[] = []; - s.tree.nodes.get(n.ref)!.children.forEach(c => nodes.push(s.objects.get(c!)!)); - return nodes; - }); - } - - registerModifier('ofType', ofType); - export function ofType(b: Selector, t: StateObject.Type) { return filter(b, n => n.obj ? n.obj.type === t : false); } - - registerModifier('ancestorOfType', ancestorOfType); - export function ancestorOfType(b: Selector, t: StateObject.Type) { return unique(mapEntity(b, (n, s) => findAncestorOfType(s, n.ref, t))); } - - registerModifier('parent', parent); - export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.objects.get(s.tree.nodes.get(n.ref)!.parent))); } - - function findAncestorOfType({ tree, objects }: State, root: string, type: StateObject.Type): StateObject.Node | undefined { - let current = tree.nodes.get(root)!; - while (true) { - current = tree.nodes.get(current.parent)!; - if (current.ref === tree.rootRef) { - return objects.get(tree.rootRef); - } - const obj = objects.get(current.ref)!.obj!; - if (obj.type === type) return objects.get(current.ref); - } - } -} - -export { StateSelection } \ No newline at end of file diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 149486e64e48613a24002e0a3aff010ee6ac01b7..5c2f27e15f4465f27b1814fe5de794571ec08fed 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -4,108 +4,171 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StateObject } from './object'; +import { StateObject, StateObjectCell } from './object'; import { StateTree } from './tree'; import { Transform } from './transform'; -import { ImmutableTree } from './util/immutable-tree'; import { Transformer } from './transformer'; -import { StateContext } from './context'; import { UUID } from 'mol-util'; import { RuntimeContext, Task } from 'mol-task'; +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'; +import { TransientTree } from './tree/transient'; +import { LogEntry } from 'mol-util/log-entry'; +import { now, formatTimespan } from 'mol-util/now'; export { State } class State { - private _tree: StateTree = StateTree.create(); - private _current: Transform.Ref = this._tree.rootRef; + private _tree: TransientTree = StateTree.createEmpty().asTransient(); + + protected errorFree = true; private transformCache = new Map<Transform.Ref, unknown>(); - get tree() { return this._tree; } - get current() { return this._current; } + private ev = RxEventHelper.create(); + + readonly globalContext: unknown = void 0; + readonly events = { + cell: { + stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State}>(), + created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(), + removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(), + }, + object: { + updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(), + created: this.ev<State.ObjectEvent & { obj: StateObject }>(), + removed: this.ev<State.ObjectEvent & { obj?: StateObject }>() + }, + log: this.ev<LogEntry>(), + changed: this.ev<void>() + }; + + readonly behaviors = { + currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef }) + }; + + readonly actions = new StateActionManager(); - readonly objects: State.Objects = new Map(); - readonly context: StateContext; + get tree(): StateTree { return this._tree; } + get current() { return this.behaviors.currentObject.value.ref; } + + build() { return this._tree.build(); } + + readonly cells: State.Cells = new Map(); getSnapshot(): State.Snapshot { - const props = Object.create(null); - const keys = this.objects.keys(); - while (true) { - const key = keys.next(); - if (key.done) break; - const o = this.objects.get(key.value)!; - props[key.value] = { ...o.props }; - } - return { - tree: StateTree.toJSON(this._tree), - props - }; + return { tree: StateTree.toJSON(this._tree) }; } setSnapshot(snapshot: State.Snapshot) { const tree = StateTree.fromJSON(snapshot.tree); - // TODO: support props and async - return this.update(tree).run(); + return this.update(tree); } setCurrent(ref: Transform.Ref) { - this._current = ref; - this.context.behaviors.currentObject.next({ ref }); + this.behaviors.currentObject.next({ state: this, ref }); + } + + updateCellState(ref: Transform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) { + const update = typeof stateOrProvider === 'function' + ? stateOrProvider(this.tree.cellStates.get(ref)) + : stateOrProvider; + + if (this._tree.updateCellState(ref, update)) { + this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); + } } dispose() { - this.context.dispose(); + this.ev.dispose(); } - update(tree: StateTree): Task<void> { - // TODO: support props + /** + * Select Cells by ref or a query generated on the fly. + * @example state.select('test') + * @example state.select(q => q.byRef('test').subtree()) + */ + select(selector: Transform.Ref | ((q: typeof StateSelection.Generators) => StateSelection.Selector)) { + if (typeof selector === 'string') return StateSelection.select(selector, this); + return StateSelection.select(selector(StateSelection.Generators), this) + } + + /** If 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> { + const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient(); return Task.create('Update Tree', async taskCtx => { + let updated = false; try { const oldTree = this._tree; - this._tree = tree; + this._tree = _tree; const ctx: UpdateContext = { - stateCtx: this.context, + parent: this, + editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0, + + errorFree: this.errorFree, taskCtx, oldTree, - tree: tree, - objects: this.objects, - transformCache: this.transformCache + tree: _tree, + cells: this.cells as Map<Transform.Ref, StateObjectCell>, + transformCache: this.transformCache, + + changed: false, + hadError: false, + newCurrent: void 0 }; - // TODO: have "cancelled" error? Or would this be handled automatically? - await update(ctx); + + this.errorFree = true; + // TODO: handle "cancelled" error? Or would this be handled automatically? + updated = await update(ctx); } finally { - this.context.events.updated.next(); + if (updated) this.events.changed.next(); } }); } - constructor(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) { + constructor(rootObject: StateObject, params?: { globalContext?: unknown }) { const tree = this._tree; - const root = tree.getValue(tree.rootRef)!; - const defaultObjectProps = (params && params.defaultObjectProps) || { } + const root = tree.root; - this.objects.set(tree.rootRef, { - ref: tree.rootRef, + (this.cells as Map<Transform.Ref, StateObjectCell>).set(root.ref, { + transform: root, + sourceRef: void 0, obj: rootObject, - state: StateObject.StateType.Ok, + status: 'ok', version: root.version, - props: { ...defaultObjectProps } + errorText: void 0 }); - this.context = new StateContext({ - globalContext: params && params.globalContext, - defaultObjectProps, - rootRef: tree.rootRef - }); + this.globalContext = params && params.globalContext; } } namespace State { - export type Objects = Map<Transform.Ref, StateObject.Node> + export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell> + + export type Tree = StateTree + export type Builder = StateTreeBuilder + + export interface ObjectEvent { + state: State, + ref: Ref + } export interface Snapshot { - readonly tree: StateTree.Serialized, - readonly props: { [key: string]: unknown } + readonly tree: StateTree.Serialized } export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) { @@ -113,212 +176,350 @@ namespace State { } } - type Ref = Transform.Ref +type Ref = Transform.Ref - interface UpdateContext { - stateCtx: StateContext, - taskCtx: RuntimeContext, - oldTree: StateTree, - tree: StateTree, - objects: State.Objects, - transformCache: Map<Ref, unknown> - } +interface UpdateContext { + parent: State, + editInfo: StateTreeBuilder.EditInfo | undefined + + errorFree: boolean, + taskCtx: RuntimeContext, + oldTree: StateTree, + tree: TransientTree, + cells: Map<Transform.Ref, StateObjectCell>, + transformCache: Map<Ref, unknown>, - async function update(ctx: UpdateContext) { - const roots = findUpdateRoots(ctx.objects, ctx.tree); - const deletes = findDeletes(ctx); + changed: boolean, + hadError: boolean, + newCurrent?: Ref +} + +async function update(ctx: UpdateContext) { + // if only a single node was added/updated, we can skip potentially expensive diffing + const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree); + + let deletes: Transform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: Transform.Ref[]; + + if (fastTrack) { + deletes = []; + roots = [ctx.editInfo!.lastUpdate!]; + } else { + // find all nodes that will definitely be deleted. + // this is done in "post order", meaning that leaves will be deleted first. + deletes = findDeletes(ctx); + + const current = ctx.parent.current; + let hasCurrent = false; for (const d of deletes) { - const obj = ctx.objects.has(d) ? ctx.objects.get(d)!.obj : void 0; - ctx.objects.delete(d); - ctx.transformCache.delete(d); - ctx.stateCtx.events.object.removed.next({ ref: d, obj }); - // TODO: handle current object change + if (d === current) { + hasCurrent = true; + break; + } } - initObjectState(ctx, roots); + if (hasCurrent) { + const newCurrent = findNewCurrent(ctx, current, deletes); + ctx.parent.setCurrent(newCurrent); + } - for (const root of roots) { - await updateSubtree(ctx, root); + for (const d of deletes) { + const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0; + ctx.cells.delete(d); + ctx.transformCache.delete(d); + deletedObjects.push(obj); } + + // Find roots where transform version changed or where nodes will be added. + roots = findUpdateRoots(ctx.cells, ctx.tree); } - function findUpdateRoots(objects: State.Objects, tree: StateTree) { - const findState = { - roots: [] as Ref[], - objects - }; + // Init empty cells where not present + // this is done in "pre order", meaning that "parents" will be created 1st. + const addedCells = initCells(ctx, roots); - ImmutableTree.doPreOrder(tree, tree.nodes.get(tree.rootRef)!, findState, (n, _, s) => { - if (!s.objects.has(n.ref)) { - s.roots.push(n.ref); - return false; - } - const o = s.objects.get(n.ref)!; - if (o.version !== n.value.version) { - s.roots.push(n.ref); - return false; - } + // Ensure cell states stay consistent + if (!ctx.editInfo) { + syncStates(ctx); + } - return true; - }); + // Notify additions of new cells. + for (const cell of addedCells) { + ctx.parent.events.cell.created.next({ state: ctx.parent, ref: cell.transform.ref, cell }); + } - return findState.roots; + for (let i = 0; i < deletes.length; i++) { + const d = deletes[i]; + const parent = ctx.oldTree.transforms.get(d).parent; + ctx.parent.events.object.removed.next({ state: ctx.parent, ref: d, obj: deletedObjects[i] }); + ctx.parent.events.cell.removed.next({ state: ctx.parent, ref: d, parent: parent }); } - function findDeletes(ctx: UpdateContext): Ref[] { - // TODO: do this in some sort of "tree order"? - const deletes: Ref[] = []; - const keys = ctx.objects.keys(); - while (true) { - const key = keys.next(); - if (key.done) break; - if (!ctx.tree.nodes.has(key.value)) deletes.push(key.value); - } - return deletes; + if (deletedObjects.length) deletedObjects = []; + + // Set status of cells that will be updated to 'pending'. + initCellStatus(ctx, roots); + + // Sequentially update all the subtrees. + for (const root of roots) { + await updateSubtree(ctx, root); } - function setObjectState(ctx: UpdateContext, ref: Ref, state: StateObject.StateType, errorText?: string) { - let changed = false; - if (ctx.objects.has(ref)) { - const obj = ctx.objects.get(ref)!; - changed = obj.state !== state; - obj.state = state; - obj.errorText = errorText; - } else { - const obj: StateObject.Node = { ref, state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } }; - ctx.objects.set(ref, obj); - changed = true; - } - if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref }); + if (ctx.newCurrent) ctx.parent.setCurrent(ctx.newCurrent); + + return deletes.length > 0 || roots.length > 0 || ctx.changed; +} + +function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) { + const findState = { roots: [] as Ref[], cells }; + StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor); + return findState.roots; +} + +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') { + s.roots.push(n.ref); + return false; } + return true; +} - function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) { - setObjectState(ctx, t.ref, StateObject.StateType.Pending); +type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] } +function checkDeleteVisitor(n: Transform, _: any, ctx: FindDeletesCtx) { + if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref); +} +function findDeletes(ctx: UpdateContext): Ref[] { + const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] }; + StateTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, checkDeleteVisitor); + return deleteCtx.deletes; +} + +function syncStatesVisitor(n: Transform, tree: StateTree, oldState: StateTree.CellStates) { + if (!oldState.has(n.ref)) return; + (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref)); +} +function syncStates(ctx: UpdateContext) { + StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncStatesVisitor); +} + +function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) { + const cell = ctx.cells.get(ref)!; + const changed = cell.status !== status; + cell.status = status; + cell.errorText = errorText; + if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) }); +} + +function initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) { + ctx.cells.get(t.ref)!.transform = t; + setCellStatus(ctx, t.ref, 'pending'); +} + +function initCellStatus(ctx: UpdateContext, roots: Ref[]) { + for (const root of roots) { + StateTree.doPreOrder(ctx.tree, ctx.tree.transforms.get(root), ctx, initCellStatusVisitor); } - /** Return "resolve set" */ - function initObjectState(ctx: UpdateContext, roots: Ref[]) { - for (const root of roots) { - ImmutableTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initVisitor); - } +} + +type InitCellsCtx = { ctx: UpdateContext, added: StateObjectCell[] } +function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCellsCtx) { + if (ctx.cells.has(transform.ref)) { + return; } - function doError(ctx: UpdateContext, ref: Ref, errorText: string) { - setObjectState(ctx, ref, StateObject.StateType.Error, errorText); - const wrap = ctx.objects.get(ref)!; - if (wrap.obj) { - ctx.stateCtx.events.object.removed.next({ ref }); - ctx.transformCache.delete(ref); - wrap.obj = void 0; - } + const cell: StateObjectCell = { + transform, + sourceRef: void 0, + status: 'pending', + version: UUID.create22(), + errorText: void 0 + }; + ctx.cells.set(transform.ref, cell); + added.push(cell); +} - const children = ctx.tree.nodes.get(ref)!.children.values(); - while (true) { - const next = children.next(); - if (next.done) return; - doError(ctx, next.value, 'Parent node contains error.'); - } +function initCells(ctx: UpdateContext, roots: Ref[]) { + const initCtx: InitCellsCtx = { ctx, added: [] }; + for (const root of roots) { + StateTree.doPreOrder(ctx.tree, ctx.tree.transforms.get(root), initCtx, initCellsVisitor); } + return initCtx.added; +} - function findAncestor(tree: StateTree, objects: State.Objects, root: Ref, types: { type: StateObject.Type }[]): StateObject { - let current = tree.nodes.get(root)!; - while (true) { - current = tree.nodes.get(current.parent)!; - if (current.ref === tree.rootRef) { - return objects.get(tree.rootRef)!.obj!; - } - const obj = objects.get(current.ref)!.obj!; - for (const t of types) if (obj.type === t.type) return objects.get(current.ref)!.obj!; +function findNewCurrent(ctx: UpdateContext, start: Ref, deletes: Ref[]) { + const deleteSet = new Set(deletes); + return _findNewCurrent(ctx.oldTree, start, deleteSet); +} + +function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref { + if (ref === Transform.RootRef) return ref; + + const node = tree.transforms.get(ref)!; + const siblings = tree.children.get(node.parent)!.values(); + + let prevCandidate: Ref | undefined = void 0, seenRef = false; + + while (true) { + const s = siblings.next(); + if (s.done) break; + + if (deletes.has(s.value)) continue; + + const t = tree.transforms.get(s.value); + if (t.props && t.props.isGhost) continue; + if (s.value === ref) { + seenRef = true; + if (!deletes.has(ref)) prevCandidate = ref; + continue; } + + if (seenRef) return t.ref; + + prevCandidate = t.ref; } - async function updateSubtree(ctx: UpdateContext, root: Ref) { - setObjectState(ctx, root, StateObject.StateType.Processing); - - try { - const update = await updateNode(ctx, root); - setObjectState(ctx, root, StateObject.StateType.Ok); - if (update.action === 'created') { - ctx.stateCtx.events.object.created.next({ ref: root, obj: update.obj! }); - } else if (update.action === 'updated') { - ctx.stateCtx.events.object.updated.next({ ref: root, obj: update.obj }); - } else if (update.action === 'replaced') { - ctx.stateCtx.events.object.replaced.next({ ref: root, oldObj: update.oldObj, newObj: update.newObj }); - } - } catch (e) { - doError(ctx, root, '' + e); - return; - } + if (prevCandidate) return prevCandidate; + return _findNewCurrent(tree, node.parent, deletes); +} - const children = ctx.tree.nodes.get(root)!.children.values(); - while (true) { - const next = children.next(); - if (next.done) return; - await updateSubtree(ctx, next.value); - } +/** Set status and error text of the cell. Remove all existing objects in the subtree. */ +function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { + ctx.hadError = true; + (ctx.parent as any as { errorFree: boolean }).errorFree = false; + + if (errorText) { + setCellStatus(ctx, ref, 'error', errorText); + ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText }); + } + + const cell = ctx.cells.get(ref)!; + if (cell.obj) { + const obj = cell.obj; + cell.obj = void 0; + ctx.parent.events.object.removed.next({ state: ctx.parent, ref, obj }); + ctx.transformCache.delete(ref); } - async function updateNode(ctx: UpdateContext, currentRef: Ref) { - const { oldTree, tree, objects } = ctx; - const transform = tree.getValue(currentRef)!; - const parent = findAncestor(tree, objects, currentRef, transform.transformer.definition.from); - // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined') - if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) { - // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef)); - const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); - objects.set(currentRef, { - ref: currentRef, - obj, - state: StateObject.StateType.Ok, - version: transform.version, - props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps } - }); - return { action: 'created', obj }; - } else { - // console.log('updating...', transform.transformer.id); - const current = objects.get(currentRef)!; - const oldParams = oldTree.getValue(currentRef)!.params; - switch (await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)) { - case Transformer.UpdateResult.Recreate: { - const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); - objects.set(currentRef, { - ref: currentRef, - obj, - state: StateObject.StateType.Ok, - version: transform.version, - props: { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps } - }); - return { action: 'replaced', oldObj: current.obj!, newObj: obj }; - } - case Transformer.UpdateResult.Updated: - current.version = transform.version; - current.props = { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps }; - return { action: 'updated', obj: current.obj }; - default: - // TODO check if props need to be updated - return { action: 'none' }; + // remove the objects in the child nodes if they exist + const children = ctx.tree.children.get(ref).values(); + while (true) { + const next = children.next(); + if (next.done) return; + doError(ctx, next.value, void 0); + } +} + +type UpdateNodeResult = + | { action: 'created', obj: StateObject } + | { action: 'updated', obj: StateObject } + | { action: 'replaced', oldObj?: StateObject, obj: StateObject } + | { action: 'none' } + +async function updateSubtree(ctx: UpdateContext, root: Ref) { + setCellStatus(ctx, root, 'processing'); + + try { + const start = now(); + const update = await updateNode(ctx, root); + const time = now() - start; + + if (update.action !== 'none') ctx.changed = true; + + setCellStatus(ctx, root, 'ok'); + if (update.action === 'created') { + ctx.parent.events.object.created.next({ state: ctx.parent, ref: root, obj: update.obj! }); + ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!ctx.hadError) { + const transform = ctx.tree.transforms.get(root); + if (!transform.props || !transform.props.isGhost) ctx.newCurrent = root; } + } else if (update.action === 'updated') { + ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'in-place', obj: update.obj }); + ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); + } else if (update.action === 'replaced') { + ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'recreate', obj: update.obj, oldObj: update.oldObj }); + ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); } + } catch (e) { + ctx.changed = true; + if (!ctx.hadError) ctx.newCurrent = root; + doError(ctx, root, '' + e); + return; } - function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) { - if (typeof (t as any).run === 'function') return (t as Task<T>).runInContext(ctx); - return t as T; + const children = ctx.tree.children.get(root).values(); + while (true) { + const next = children.next(); + if (next.done) return; + await updateSubtree(ctx, next.value); } +} - function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) { - const cache = { }; - ctx.transformCache.set(ref, cache); - return runTask(transformer.definition.apply({ a, params, cache }, ctx.stateCtx.globalContext), ctx.taskCtx); +async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNodeResult> { + const { oldTree, tree } = ctx; + const current = ctx.cells.get(currentRef)!; + const transform = current.transform; + + // special case for Root + if (current.transform.ref === Transform.RootRef) return { action: 'none' }; + + const parentCell = StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from); + if (!parentCell) { + throw new Error(`No suitable parent found for '${currentRef}'`); } - async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { - if (!transformer.definition.update) { - return Transformer.UpdateResult.Recreate; - } - let cache = ctx.transformCache.get(ref); - if (!cache) { - cache = { }; - ctx.transformCache.set(ref, cache); + const parent = parentCell.obj!; + current.sourceRef = parentCell.transform.ref; + + if (!oldTree.transforms.has(currentRef)) { + const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); + current.obj = obj; + current.version = transform.version; + + return { action: 'created', obj }; + } else { + const oldParams = oldTree.transforms.get(currentRef)!.params; + + const updateKind = !!current.obj + ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params) + : Transformer.UpdateResult.Recreate; + + switch (updateKind) { + case Transformer.UpdateResult.Recreate: { + const oldObj = current.obj; + const newObj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); + current.obj = newObj; + current.version = transform.version; + return { action: 'replaced', oldObj, obj: newObj }; + } + case Transformer.UpdateResult.Updated: + current.version = transform.version; + return { action: 'updated', obj: current.obj! }; + default: + return { action: 'none' }; } - return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx); - } \ No newline at end of file + } +} + +function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) { + if (typeof (t as any).runInContext === 'function') return (t as Task<T>).runInContext(ctx); + return t as T; +} + +function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) { + const cache = Object.create(null); + ctx.transformCache.set(ref, cache); + return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx); +} + +async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { + if (!transformer.definition.update) { + return Transformer.UpdateResult.Recreate; + } + let cache = ctx.transformCache.get(ref); + if (!cache) { + cache = Object.create(null); + ctx.transformCache.set(ref, cache); + } + return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.parent.globalContext), ctx.taskCtx); +} \ No newline at end of file diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts new file mode 100644 index 0000000000000000000000000000000000000000..d25a82835478a7ea909a0f6b30e63d2203f9b7a0 --- /dev/null +++ b/src/mol-state/state/selection.ts @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateObject, StateObjectCell } from '../object'; +import { State } from '../state'; +import { StateTree } from '../tree'; +import { Transform } from '../transform'; + +namespace StateSelection { + export type Selector = Query | Builder | string | StateObjectCell; + export type CellSeq = StateObjectCell[] + export type Query = (state: State) => CellSeq; + + export function select(s: Selector, state: State) { + return compile(s)(state); + } + + export function compile(s: Selector): Query { + const selector = s ? s : Generators.root; + let query: Query; + if (isBuilder(selector)) query = (selector as any).compile(); + else if (isObj(selector)) query = (Generators.byValue(selector) as any).compile(); + else if (isQuery(selector)) query = selector; + else query = (Generators.byRef(selector as string) as any).compile(); + return query; + } + + function isObj(arg: any): arg is StateObjectCell { + return (arg as StateObjectCell).version !== void 0; + } + + function isBuilder(arg: any): arg is Builder { + return arg.compile !== void 0; + } + + function isQuery(arg: any): arg is Query { + return typeof arg === 'function'; + } + + export interface Builder { + flatMap(f: (n: StateObjectCell) => StateObjectCell[]): Builder; + mapEntity(f: (n: StateObjectCell) => StateObjectCell): Builder; + unique(): Builder; + + parent(): Builder; + first(): Builder; + filter(p: (n: StateObjectCell) => boolean): Builder; + withStatus(s: StateObjectCell.Status): Builder; + subtree(): Builder; + children(): Builder; + ofType(t: StateObject.Ctor): Builder; + ancestorOfType(t: StateObject.Ctor): Builder; + + select(state: State): CellSeq + } + + const BuilderPrototype: any = { + select(state?: State) { + return select(this, state || this.state); + } + }; + + function registerModifier(name: string, f: Function) { + BuilderPrototype[name] = function (this: any, ...args: any[]) { return f.call(void 0, this, ...args) }; + } + + function build(compile: () => Query): Builder { + return Object.create(BuilderPrototype, { compile: { writable: false, configurable: false, value: compile } }); + } + + export namespace Generators { + export const root = build(() => (state: State) => [state.cells.get(state.tree.root.ref)!]); + + export function byRef(...refs: Transform.Ref[]) { + return build(() => (state: State) => { + const ret: StateObjectCell[] = []; + for (const ref of refs) { + const n = state.cells.get(ref); + if (!n) continue; + ret.push(n); + } + return ret; + }); + } + + 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); + export function flatMap(b: Selector, f: (obj: StateObjectCell, state: State) => CellSeq) { + const q = compile(b); + return build(() => (state: State) => { + const ret: StateObjectCell[] = []; + for (const n of q(state)) { + for (const m of f(n, state)) { + ret.push(m); + } + } + return ret; + }); + } + + registerModifier('mapEntity', mapEntity); + export function mapEntity(b: Selector, f: (n: StateObjectCell, state: State) => StateObjectCell | undefined) { + const q = compile(b); + return build(() => (state: State) => { + const ret: StateObjectCell[] = []; + for (const n of q(state)) { + const x = f(n, state); + if (x) ret.push(x); + } + return ret; + }); + } + + registerModifier('unique', unique); + export function unique(b: Selector) { + const q = compile(b); + return build(() => (state: State) => { + const set = new Set<string>(); + const ret: StateObjectCell[] = []; + for (const n of q(state)) { + if (!n) continue; + if (!set.has(n.transform.ref)) { + set.add(n.transform.ref); + ret.push(n); + } + } + return ret; + }) + } + + registerModifier('first', first); + export function first(b: Selector) { + const q = compile(b); + return build(() => (state: State) => { + const r = q(state); + return r.length ? [r[0]] : []; + }); + } + + registerModifier('filter', filter); + export function filter(b: Selector, p: (n: StateObjectCell) => boolean) { return flatMap(b, n => p(n) ? [n] : []); } + + registerModifier('withStatus', withStatus); + export function withStatus(b: Selector, s: StateObjectCell.Status) { return filter(b, n => n.status === s); } + + registerModifier('subtree', subtree); + export function subtree(b: Selector) { + return flatMap(b, (n, s) => { + const nodes = [] as string[]; + StateTree.doPreOrder(s.tree, s.tree.transforms.get(n.transform.ref), nodes, (x, _, ctx) => { ctx.push(x.ref) }); + return nodes.map(x => s.cells.get(x)!); + }); + } + + registerModifier('children', children); + export function children(b: Selector) { + return flatMap(b, (n, s) => { + const nodes: StateObjectCell[] = []; + s.tree.children.get(n.transform.ref).forEach(c => nodes.push(s.cells.get(c!)!)); + return nodes; + }); + } + + registerModifier('ofType', ofType); + export function ofType(b: Selector, t: StateObject.Ctor) { return filter(b, n => n.obj ? n.obj.type === t.type : false); } + + registerModifier('ancestorOfType', ancestorOfType); + export function ancestorOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapEntity(b, (n, s) => findAncestorOfType(s.tree, s.cells, n.transform.ref, types))); } + + registerModifier('parent', parent); + export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); } + + export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: Transform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined { + let current = tree.transforms.get(root)!, len = types.length; + while (true) { + current = tree.transforms.get(current.parent)!; + const cell = cells.get(current.ref)!; + if (!cell.obj) return void 0; + const obj = cell.obj; + for (let i = 0; i < len; i++) { + if (obj.type === types[i].type) return cells.get(current.ref); + } + if (current.ref === Transform.RootRef) { + return void 0; + } + } + } +} + +export { StateSelection } \ No newline at end of file diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts index f80137241fc821a9cf9dc49e3759da3a0ae2040d..bda45d3315c9b945aa8b34b36bd9bb85d4dfe2d7 100644 --- a/src/mol-state/transform.ts +++ b/src/mol-state/transform.ts @@ -9,43 +9,57 @@ import { Transformer } from './transformer'; import { UUID } from 'mol-util'; export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { + readonly parent: Transform.Ref, readonly transformer: Transformer<A, B, P>, - readonly params: P, + readonly props: Transform.Props, readonly ref: Transform.Ref, - readonly version: string, - readonly defaultProps?: unknown + readonly params: P, + readonly version: string } export namespace Transform { export type Ref = string - export interface Options { ref?: Ref, defaultProps?: unknown } + export const RootRef = '-=root=-' as Ref; - export function create<A extends StateObject, B extends StateObject, P>(transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> { - const ref = options && options.ref ? options.ref : UUID.create() as string as Ref; + export interface Props { + tag?: string + isGhost?: boolean, + isBinding?: boolean + } + + export interface Options { + ref?: string, + props?: Props + } + + export function create<A extends StateObject, B extends StateObject, P>(parent: Ref, transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> { + const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref; return { + parent, transformer, - params: params || {} as any, + props: (options && options.props) || { }, ref, - version: UUID.create(), - defaultProps: options && options.defaultProps + params: params || {} as any, + version: UUID.create22() } } - export function updateParams<T>(t: Transform, params: any): Transform { - return { ...t, params, version: UUID.create() }; + export function withParams<T>(t: Transform, params: any): Transform { + return { ...t, params, version: UUID.create22() }; } - export function createRoot(ref: Ref): Transform { - return create(Transformer.ROOT, {}, { ref }); + export function createRoot(): Transform { + return create(RootRef, Transformer.ROOT, {}, { ref: RootRef }); } export interface Serialized { + parent: string, transformer: string, params: any, + props: Props, ref: string, - version: string, - defaultProps?: unknown + version: string } function _id(x: any) { return x; } @@ -54,11 +68,12 @@ export namespace Transform { ? t.transformer.definition.customSerialization.toJSON : _id; return { + parent: t.parent, transformer: t.transformer.id, params: pToJson(t.params), + props: t.props, ref: t.ref, - version: t.version, - defaultProps: t.defaultProps + version: t.version }; } @@ -68,11 +83,12 @@ export namespace Transform { ? transformer.definition.customSerialization.toJSON : _id; return { + parent: t.parent as Ref, transformer, params: pFromJson(t.params), - ref: t.ref, - version: t.version, - defaultProps: t.defaultProps + props: t.props, + ref: t.ref as Ref, + version: t.version }; } } \ No newline at end of file diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index d8727cfc92995351a3240959bedee94a8d87de17..6588a55167bbc472d69cfb56a3cd1335ba8ea8bb 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -8,9 +8,11 @@ import { Task } from 'mol-task'; import { StateObject } from './object'; import { Transform } from './transform'; import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { StateAction } from './action'; export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { - apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, + apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, + toAction(): StateAction<A, void, P>, readonly namespace: string, readonly id: Transformer.Id, readonly definition: Transformer.Definition<A, B, P> @@ -19,9 +21,14 @@ export interface Transformer<A extends StateObject = StateObject, B extends Stat export namespace Transformer { export type Id = string & { '@type': 'transformer-id' } export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown; + export type From<T extends Transformer<any, any, any>> = T extends Transformer<infer A, any, any> ? A : unknown; export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown; export type ControlsFor<Props> = { [P in keyof Props]?: PD.Any } + export function is(obj: any): obj is Transformer { + return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function'; + } + export interface ApplyParams<A extends StateObject = StateObject, P = unknown> { a: A, params: P, @@ -40,10 +47,21 @@ export namespace Transformer { export enum UpdateResult { Unchanged, Updated, Recreate } + export interface ParamsDefinition<A extends StateObject = StateObject, P = unknown> { + /** Check the parameters and return a list of errors if the are not valid. */ + default?(a: A, globalCtx: unknown): P, + /** Specify default control descriptors for the parameters */ + controls?(a: A, globalCtx: unknown): ControlsFor<P>, + /** Check the parameters and return a list of errors if the are not valid. */ + validate?(params: P, a: A, globalCtx: unknown): string[] | undefined, + /** Optional custom parameter equality. Use deep structural equal by default. */ + areEqual?(oldParams: P, newParams: P): boolean + } + export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { readonly name: string, - readonly from: { type: StateObject.Type }[], - readonly to: { type: StateObject.Type }[], + readonly from: StateObject.Ctor[], + readonly to: StateObject.Ctor[], readonly display?: { readonly name: string, readonly description?: string }, /** @@ -59,16 +77,7 @@ export namespace Transformer { */ update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult, - params?: { - /** Check the parameters and return a list of errors if the are not valid. */ - default?(a: A, globalCtx: unknown): P, - /** Specify default control descriptors for the parameters */ - controls?(a: A, globalCtx: unknown): ControlsFor<P>, - /** Check the parameters and return a list of errors if the are not valid. */ - validate?(a: A, params: P, globalCtx: unknown): string[] | undefined, - /** Optional custom parameter equality. Use deep structural equal by default. */ - areEqual?(oldParams: P, newParams: P): boolean - } + readonly params?: ParamsDefinition<A, P>, /** Test if the transform can be applied to a given node */ isApplicable?(a: A, globalCtx: unknown): boolean, @@ -77,7 +86,7 @@ export namespace Transformer { isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string }, /** Custom conversion to and from JSON */ - customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } + readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } } const registry = new Map<Id, Transformer<any, any>>(); @@ -114,7 +123,8 @@ export namespace Transformer { } const t: Transformer<A, B, P> = { - apply(params, props) { return Transform.create<A, B, P>(t as any, params, props); }, + apply(parent, params, props) { return Transform.create<A, B, P>(parent, t, params, props); }, + toAction() { return StateAction.fromTransformer(t); }, namespace, id, definition diff --git a/src/mol-state/tree.ts b/src/mol-state/tree.ts index f7fbd4955dae802b5bf23fb5535ba8370cd03a7b..837524f80a910c6006e8a8790ddda132431507ff 100644 --- a/src/mol-state/tree.ts +++ b/src/mol-state/tree.ts @@ -4,82 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Transform } from './transform'; -import { ImmutableTree } from './util/immutable-tree'; -import { Transformer } from './transformer'; -import { StateObject } from './object'; +import { StateTree } from './tree/immutable'; +import { TransientTree } from './tree/transient'; -interface StateTree extends ImmutableTree<Transform> { } - -namespace StateTree { - export interface Transient extends ImmutableTree.Transient<Transform> { } - export interface Serialized extends ImmutableTree.Serialized { } - - function _getRef(t: Transform) { return t.ref; } - - export function create() { - return ImmutableTree.create<Transform>(Transform.createRoot('<:root:>'), _getRef); - } - - export function updateParams<T extends Transformer = Transformer>(tree: StateTree, ref: Transform.Ref, params: Transformer.Params<T>): StateTree { - const t = tree.nodes.get(ref)!.value; - const newTransform = Transform.updateParams(t, params); - const newTree = ImmutableTree.asTransient(tree); - newTree.setValue(ref, newTransform); - return newTree.asImmutable(); - } - - export function toJSON(tree: StateTree) { - return ImmutableTree.toJSON(tree, Transform.toJSON) as Serialized; - } - - export function fromJSON(data: Serialized): StateTree { - return ImmutableTree.fromJSON(data, _getRef, Transform.fromJSON); - } - - export interface Builder { - getTree(): StateTree - } - - export function build(tree: StateTree) { - return new Builder.Root(tree); - } - - export namespace Builder { - interface State { - tree: StateTree.Transient - } - - export class Root implements Builder { - private state: State; - to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); } - toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.rootRef as any, this); } - delete(ref: Transform.Ref) { - this.state.tree.remove(ref); - return this; - } - getTree(): StateTree { return this.state.tree.asImmutable(); } - constructor(tree: StateTree) { this.state = { tree: ImmutableTree.asTransient(tree) } } - } - - export class To<A extends StateObject> implements Builder { - apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Options>): To<Transformer.To<T>> { - const t = tr.apply(params, props); - this.state.tree.add(this.ref, t); - return new To(this.state, t.ref, this.root); - } - - and() { return this.root; } - - getTree(): StateTree { return this.state.tree.asImmutable(); } - - constructor(private state: State, private ref: Transform.Ref, private root: Root) { - if (!this.state.tree.nodes.has(ref)) { - throw new Error(`Could not find node '${ref}'.`); - } - } - } - } -} - -export { StateTree } \ No newline at end of file +export { StateTree, TransientTree } \ No newline at end of file diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ce875ef16c8aa4b52962fb35f9f48b1a519a1fc --- /dev/null +++ b/src/mol-state/tree/builder.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateTree } from './immutable'; +import { TransientTree } from './transient'; +import { StateObject, StateObjectCell } from '../object'; +import { Transform } from '../transform'; +import { Transformer } from '../transformer'; + +export { StateTreeBuilder } + +interface StateTreeBuilder { + readonly editInfo: StateTreeBuilder.EditInfo, + getTree(): StateTree +} + +namespace StateTreeBuilder { + export interface EditInfo { + sourceTree: StateTree, + count: number, + lastUpdate?: Transform.Ref + } + + interface State { + tree: TransientTree, + editInfo: EditInfo + } + + export function is(obj: any): obj is StateTreeBuilder { + return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function'; + } + + export class Root implements StateTreeBuilder { + private state: State; + get editInfo() { return this.state.editInfo; } + + to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); } + toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); } + delete(ref: Transform.Ref) { + this.editInfo.count++; + this.state.tree.remove(ref); + return this; + } + getTree(): StateTree { return this.state.tree.asImmutable(); } + constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } } + } + + export class To<A extends StateObject> implements StateTreeBuilder { + get editInfo() { return this.state.editInfo; } + + apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, options?: Partial<Transform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<Transformer.To<T>> { + const t = tr.apply(this.ref, params, options); + this.state.tree.add(t, initialCellState); + this.editInfo.count++; + this.editInfo.lastUpdate = t.ref; + return new To(this.state, t.ref, this.root); + } + + update<T extends Transformer<A, any, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root + update(params: any): Root + update<T extends Transformer<A, any, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) { + let params: any; + if (provider) { + const old = this.state.tree.transforms.get(this.ref)!; + params = provider(old.params as any); + } else { + params = paramsOrTransformer; + } + + if (this.state.tree.setParams(this.ref, params)) { + this.editInfo.count++; + this.editInfo.lastUpdate = this.ref; + } + + return this.root; + } + + to<A extends StateObject>(ref: Transform.Ref) { return this.root.to<A>(ref); } + toRoot<A extends StateObject>() { return this.root.toRoot<A>(); } + delete(ref: Transform.Ref) { return this.root.delete(ref); } + + getTree(): StateTree { return this.state.tree.asImmutable(); } + + constructor(private state: State, private ref: Transform.Ref, private root: Root) { + if (!this.state.tree.transforms.has(ref)) { + throw new Error(`Could not find node '${ref}'.`); + } + } + } +} \ No newline at end of file diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d77221c278c662fd785604d64a8e3093084d3a6 --- /dev/null +++ b/src/mol-state/tree/immutable.ts @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Map as ImmutableMap, OrderedSet } from 'immutable'; +import { Transform } from '../transform'; +import { TransientTree } from './transient'; +import { StateTreeBuilder } from './builder'; +import { StateObjectCell } from 'mol-state/object'; + +export { StateTree } + +/** + * An immutable tree where each node requires a unique reference. + * Represented as an immutable map. + */ +interface StateTree { + readonly root: Transform, + readonly transforms: StateTree.Transforms, + readonly children: StateTree.Children, + readonly cellStates: StateTree.CellStates, + + asTransient(): TransientTree, + build(): StateTreeBuilder.Root +} + +namespace StateTree { + type Ref = Transform.Ref + + export interface ChildSet { + readonly size: number, + readonly values: OrderedSet<Ref>['values'], + has(ref: Ref): boolean, + readonly forEach: OrderedSet<Ref>['forEach'], + readonly map: OrderedSet<Ref>['map'] + } + + interface _Map<T> { + readonly size: number, + has(ref: Ref): boolean, + get(ref: Ref): T + } + + export interface Transforms extends _Map<Transform> {} + export interface Children extends _Map<ChildSet> { } + export interface CellStates extends _Map<StateObjectCell.State> { } + + class Impl implements StateTree { + get root() { return this.transforms.get(Transform.RootRef)! } + + asTransient(): TransientTree { + return new TransientTree(this); + } + + build(): StateTreeBuilder.Root { + return new StateTreeBuilder.Root(this); + } + + constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) { + } + } + + /** + * Create an instance of an immutable tree. + */ + export function createEmpty(): StateTree { + const root = Transform.createRoot(); + return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]])); + } + + export function create(nodes: Transforms, children: Children, cellStates: CellStates): StateTree { + return new Impl(nodes, children, cellStates); + } + + type VisitorCtx = { tree: StateTree, state: any, f: (node: Transform, tree: StateTree, state: any) => boolean | undefined | void }; + + function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.transforms.get(c!)!); } + function _doPostOrder(ctx: VisitorCtx, root: Transform) { + const children = ctx.tree.children.get(root.ref); + if (children && children.size) { + children.forEach(_postOrderFunc, ctx); + } + ctx.f(root, ctx.tree, ctx.state); + } + + /** + * Visit all nodes in a subtree in "post order", meaning leafs get visited first. + */ + export function doPostOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S { + const ctx: VisitorCtx = { tree, state, f }; + _doPostOrder(ctx, root); + return ctx.state; + } + + function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.transforms.get(c!)!); } + function _doPreOrder(ctx: VisitorCtx, root: Transform) { + const ret = ctx.f(root, ctx.tree, ctx.state); + if (typeof ret === 'boolean' && !ret) return; + const children = ctx.tree.children.get(root.ref); + if (children && children.size) { + children.forEach(_preOrderFunc, ctx); + } + } + + /** + * Visit all nodes in a subtree in "pre order", meaning leafs get visited last. + * If the visitor function returns false, the visiting for that branch is interrupted. + */ + export function doPreOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S { + const ctx: VisitorCtx = { tree, state, f }; + _doPreOrder(ctx, root); + return ctx.state; + } + + function _subtree(n: Transform, _: any, subtree: Transform[]) { subtree.push(n); } + /** + * Get all nodes in a subtree, leafs come first. + */ + export function subtreePostOrder<T>(tree: StateTree, root: Transform) { + return doPostOrder<Transform[]>(tree, root, [], _subtree); + } + + function _visitNodeToJson(node: Transform, tree: StateTree, ctx: [Transform.Serialized, StateObjectCell.State][]) { + // const children: Ref[] = []; + // tree.children.get(node.ref).forEach(_visitChildToJson as any, children); + ctx.push([Transform.toJSON(node), tree.cellStates.get(node.ref)]); + } + + export interface Serialized { + /** Transforms serialized in pre-order */ + transforms: [Transform.Serialized, StateObjectCell.State][] + } + + export function toJSON<T>(tree: StateTree): Serialized { + const transforms: [Transform.Serialized, StateObjectCell.State][] = []; + doPreOrder(tree, tree.root, transforms, _visitNodeToJson); + return { transforms }; + } + + export function fromJSON<T>(data: Serialized): StateTree { + const nodes = ImmutableMap<Ref, Transform>().asMutable(); + const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable(); + const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable(); + + for (const t of data.transforms) { + const transform = Transform.fromJSON(t[0]); + nodes.set(transform.ref, transform); + cellStates.set(transform.ref, t[1]); + + if (!children.has(transform.ref)) { + children.set(transform.ref, OrderedSet<Ref>().asMutable()); + } + + if (transform.ref !== transform.parent) children.get(transform.parent).add(transform.ref); + } + + for (const t of data.transforms) { + const ref = t[0].ref; + children.set(ref, children.get(ref).asImmutable()); + } + + return create(nodes.asImmutable(), children.asImmutable(), cellStates.asImmutable()); + } + + export function dump(tree: StateTree) { + console.log({ + tr: (tree.transforms as ImmutableMap<any, any>).keySeq().toArray(), + tr1: (tree.transforms as ImmutableMap<any, any>).valueSeq().toArray().map(t => t.ref), + ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray(), + cs: (tree.cellStates as ImmutableMap<any, any>).keySeq().toArray() + }); + } +} \ No newline at end of file diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts new file mode 100644 index 0000000000000000000000000000000000000000..39c40fdf86a4f24d8cbb09a03feeda2ca773e029 --- /dev/null +++ b/src/mol-state/tree/transient.ts @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Map as ImmutableMap, OrderedSet } from 'immutable'; +import { Transform } from '../transform'; +import { StateTree } from './immutable'; +import { StateTreeBuilder } from './builder'; +import { StateObjectCell } from 'mol-state/object'; +import { shallowEqual } from 'mol-util/object'; + +export { TransientTree } + +class TransientTree implements StateTree { + transforms = this.tree.transforms as ImmutableMap<Transform.Ref, Transform>; + children = this.tree.children as ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>; + cellStates = this.tree.cellStates as ImmutableMap<Transform.Ref, StateObjectCell.State>; + + private changedNodes = false; + private changedChildren = false; + private changedStates = false; + + private _childMutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> | undefined = void 0; + + private get childMutations() { + if (this._childMutations) return this._childMutations; + this._childMutations = new Map(); + return this._childMutations; + } + + private changeStates() { + if (this.changedStates) return; + this.changedStates = true; + this.cellStates = this.cellStates.asMutable(); + } + + private changeNodes() { + if (this.changedNodes) return; + this.changedNodes = true; + this.transforms = this.transforms.asMutable(); + } + + private changeChildren() { + if (this.changedChildren) return; + this.changedChildren = true; + this.children = this.children.asMutable(); + } + + get root() { return this.transforms.get(Transform.RootRef)! } + + build(): StateTreeBuilder.Root { + return new StateTreeBuilder.Root(this); + } + + asTransient() { + return this.asImmutable().asTransient(); + } + + private addChild(parent: Transform.Ref, child: Transform.Ref) { + this.changeChildren(); + + if (this.childMutations.has(parent)) { + this.childMutations.get(parent)!.add(child); + } else { + const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable(); + set.add(child); + this.children.set(parent, set); + this.childMutations.set(parent, set); + } + } + + private removeChild(parent: Transform.Ref, child: Transform.Ref) { + this.changeChildren(); + + if (this.childMutations.has(parent)) { + this.childMutations.get(parent)!.remove(child); + } else { + const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable(); + set.remove(child); + this.children.set(parent, set); + this.childMutations.set(parent, set); + } + } + + private clearRoot() { + const parent = Transform.RootRef; + if (this.children.get(parent).size === 0) return; + + this.changeChildren(); + + const set = OrderedSet<Transform.Ref>(); + this.children.set(parent, set); + this.childMutations.set(parent, set); + } + + add(transform: Transform, initialState?: Partial<StateObjectCell.State>) { + const ref = transform.ref; + + if (this.transforms.has(transform.ref)) { + const node = this.transforms.get(transform.ref); + if (node.parent !== transform.parent) alreadyPresent(transform.ref); + } + + const children = this.children.get(transform.parent); + if (!children) parentNotPresent(transform.parent); + + if (!children.has(transform.ref)) { + this.addChild(transform.parent, transform.ref); + } + + if (!this.children.has(transform.ref)) { + if (!this.changedChildren) { + this.changedChildren = true; + this.children = this.children.asMutable(); + } + this.children.set(transform.ref, OrderedSet()); + } + + this.changeNodes(); + this.transforms.set(ref, transform); + + if (!this.cellStates.has(ref)) { + this.changeStates(); + if (StateObjectCell.isStateChange(StateObjectCell.DefaultState, initialState)) { + this.cellStates.set(ref, { ...StateObjectCell.DefaultState, ...initialState }); + } else { + this.cellStates.set(ref, StateObjectCell.DefaultState); + } + } + + return this; + } + + /** Calls Transform.definition.params.areEqual if available, otherwise uses shallowEqual to check if the params changed */ + setParams(ref: Transform.Ref, params: unknown) { + ensurePresent(this.transforms, ref); + + const transform = this.transforms.get(ref)!; + const def = transform.transformer.definition; + if (def.params && def.params.areEqual) { + if (def.params.areEqual(transform.params, params)) return false; + } else { + if (shallowEqual(transform.params, params)) { + return false; + } + } + + if (!this.changedNodes) { + this.changedNodes = true; + this.transforms = this.transforms.asMutable(); + } + + this.transforms.set(transform.ref, Transform.withParams(transform, params)); + return true; + } + + updateCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) { + ensurePresent(this.transforms, ref); + + const old = this.cellStates.get(ref); + if (!StateObjectCell.isStateChange(old, state)) return false; + + this.changeStates(); + this.cellStates.set(ref, { ...old, ...state }); + + return true; + } + + remove(ref: Transform.Ref): Transform[] { + const node = this.transforms.get(ref); + if (!node) return []; + + const st = StateTree.subtreePostOrder(this, node); + if (ref === Transform.RootRef) { + st.pop(); + if (st.length === 0) return st; + this.clearRoot(); + } else { + if (st.length === 0) return st; + this.removeChild(node.parent, node.ref); + } + + this.changeNodes(); + this.changeChildren(); + this.changeStates(); + + for (const n of st) { + this.transforms.delete(n.ref); + this.children.delete(n.ref); + this.cellStates.delete(n.ref); + if (this._childMutations) this._childMutations.delete(n.ref); + } + + return st; + } + + asImmutable() { + if (!this.changedNodes && !this.changedChildren && !this.changedStates && !this._childMutations) return this.tree; + if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children); + return StateTree.create( + this.changedNodes ? this.transforms.asImmutable() : this.transforms, + this.changedChildren ? this.children.asImmutable() : this.children, + this.changedStates ? this.cellStates.asImmutable() : this.cellStates); + } + + constructor(private tree: StateTree) { + + } +} + +function fixChildMutations(this: ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); } + +function alreadyPresent(ref: Transform.Ref) { + throw new Error(`Transform '${ref}' is already present in the tree.`); +} + +function parentNotPresent(ref: Transform.Ref) { + throw new Error(`Parent '${ref}' must be present in the tree.`); +} + +function ensurePresent(nodes: StateTree.Transforms, ref: Transform.Ref) { + if (!nodes.has(ref)) { + throw new Error(`Node '${ref}' is not present in the tree.`); + } +} \ No newline at end of file diff --git a/src/mol-state/util/immutable-tree.ts b/src/mol-state/util/immutable-tree.ts deleted file mode 100644 index dd7ce8f90afa53c5580857edfff29f1b585e1c4f..0000000000000000000000000000000000000000 --- a/src/mol-state/util/immutable-tree.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { Map as ImmutableMap, OrderedSet } from 'immutable'; - -/** - * An immutable tree where each node requires a unique reference. - * Represented as an immutable map. - */ -export interface ImmutableTree<T> { - readonly rootRef: ImmutableTree.Ref, - readonly version: number, - readonly nodes: ImmutableTree.Nodes<T>, - getRef(e: T): ImmutableTree.Ref, - getValue(ref: ImmutableTree.Ref): T | undefined -} - -export namespace ImmutableTree { - export type Ref = string - export interface MutableNode<T> { ref: ImmutableTree.Ref, value: T, version: number, parent: ImmutableTree.Ref, children: OrderedSet<ImmutableTree.Ref> } - export interface Node<T> extends Readonly<MutableNode<T>> { } - export interface Nodes<T> extends ImmutableMap<ImmutableTree.Ref, Node<T>> { } - - class Impl<T> implements ImmutableTree<T> { - readonly rootRef: ImmutableTree.Ref; - readonly version: number; - readonly nodes: ImmutableTree.Nodes<T>; - readonly getRef: (e: T) => ImmutableTree.Ref; - - getValue(ref: Ref) { - const n = this.nodes.get(ref); - return n ? n.value : void 0; - } - - constructor(rootRef: ImmutableTree.Ref, nodes: ImmutableTree.Nodes<T>, getRef: (e: T) => ImmutableTree.Ref, version: number) { - this.rootRef = rootRef; - this.nodes = nodes; - this.getRef = getRef; - this.version = version; - } - } - - /** - * Create an instance of an immutable tree. - */ - export function create<T>(root: T, getRef: (t: T) => ImmutableTree.Ref): ImmutableTree<T> { - const ref = getRef(root); - const r: Node<T> = { ref, value: root, version: 0, parent: ref, children: OrderedSet() }; - return new Impl(ref, ImmutableMap([[ref, r]]), getRef, 0); - } - - export function asTransient<T>(tree: ImmutableTree<T>) { - return new Transient(tree); - } - - type N = Node<any> - type Ns = Nodes<any> - - type VisitorCtx = { nodes: Ns, state: any, f: (node: N, nodes: Ns, state: any) => boolean | undefined | void }; - - function _postOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPostOrder(this, this.nodes.get(c!)!); } - function _doPostOrder(ctx: VisitorCtx, root: N) { - if (root.children.size) { - root.children.forEach(_postOrderFunc, ctx); - } - ctx.f(root, ctx.nodes, ctx.state); - } - - /** - * Visit all nodes in a subtree in "post order", meaning leafs get visited first. - */ - export function doPostOrder<T, S>(tree: ImmutableTree<T>, root: Node<T>, state: S, f: (node: Node<T>, nodes: Nodes<T>, state: S) => boolean | undefined | void) { - const ctx: VisitorCtx = { nodes: tree.nodes, state, f }; - _doPostOrder(ctx, root); - return ctx.state; - } - - function _preOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPreOrder(this, this.nodes.get(c!)!); } - function _doPreOrder(ctx: VisitorCtx, root: N) { - const ret = ctx.f(root, ctx.nodes, ctx.state); - if (typeof ret === 'boolean' && !ret) return; - if (root.children.size) { - root.children.forEach(_preOrderFunc, ctx); - } - } - - /** - * Visit all nodes in a subtree in "pre order", meaning leafs get visited last. - * If the visitor function returns false, the visiting for that branch is interrupted. - */ - export function doPreOrder<T, S>(tree: ImmutableTree<T>, root: Node<T>, state: S, f: (node: Node<T>, nodes: Nodes<T>, state: S) => boolean | undefined | void) { - const ctx: VisitorCtx = { nodes: tree.nodes, state, f }; - _doPreOrder(ctx, root); - return ctx.state; - } - - function _subtree(n: N, nodes: Ns, subtree: N[]) { subtree.push(n); } - /** - * Get all nodes in a subtree, leafs come first. - */ - export function subtreePostOrder<T>(tree: ImmutableTree<T>, root: Node<T>) { - return doPostOrder<T, Node<T>[]>(tree, root, [], _subtree); - } - - - function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); } - interface ToJsonCtx { nodes: Ref[], parent: any, children: any, values: any, valueToJSON: (v: any) => any } - function _visitNodeToJson(this: ToJsonCtx, node: Node<any>) { - this.nodes.push(node.ref); - const children: Ref[] = []; - node.children.forEach(_visitChildToJson as any, children); - this.parent[node.ref] = node.parent; - this.children[node.ref] = children; - this.values[node.ref] = this.valueToJSON(node.value); - } - - export interface Serialized { - root: Ref, - nodes: Ref[], - parent: { [key: string]: string }, - children: { [key: string]: any }, - values: { [key: string]: any } - } - - export function toJSON<T>(tree: ImmutableTree<T>, valueToJSON: (v: T) => any): Serialized { - const ctx: ToJsonCtx = { nodes: [], parent: { }, children: {}, values: {}, valueToJSON }; - tree.nodes.forEach(_visitNodeToJson as any, ctx); - return { - root: tree.rootRef, - nodes: ctx.nodes, - parent: ctx.parent, - children: ctx.children, - values: ctx.values - }; - } - - export function fromJSON<T>(data: Serialized, getRef: (v: T) => Ref, valueFromJSON: (v: any) => T): ImmutableTree<T> { - const nodes = ImmutableMap<ImmutableTree.Ref, Node<T>>().asMutable(); - for (const ref of data.nodes) { - nodes.set(ref, { - ref, - value: valueFromJSON(data.values[ref]), - version: 0, - parent: data.parent[ref], - children: OrderedSet(data.children[ref]) - }); - } - return new Impl(data.root, nodes.asImmutable(), getRef, 0); - } - - function checkSetRef(oldRef: ImmutableTree.Ref, newRef: ImmutableTree.Ref) { - if (oldRef !== newRef) { - throw new Error(`Cannot setValue of node '${oldRef}' because the new value has a different ref '${newRef}'.`); - } - } - - function ensureNotPresent(nodes: Ns, ref: ImmutableTree.Ref) { - if (nodes.has(ref)) { - throw new Error(`Cannot add node '${ref}' because a different node with this ref already present in the tree.`); - } - } - - function ensurePresent(nodes: Ns, ref: ImmutableTree.Ref) { - if (!nodes.has(ref)) { - throw new Error(`Node '${ref}' is not present in the tree.`); - } - } - - function mutateNode(nodes: Ns, mutations: Map<ImmutableTree.Ref, N>, ref: ImmutableTree.Ref): N { - ensurePresent(nodes, ref); - if (mutations.has(ref)) { - return mutations.get(ref)!; - } - const node = nodes.get(ref)!; - const newNode: N = { ref: node.ref, value: node.value, version: node.version + 1, parent: node.parent, children: node.children.asMutable() }; - mutations.set(ref, newNode); - nodes.set(ref, newNode); - return newNode; - } - - export class Transient<T> implements ImmutableTree<T> { - nodes = this.tree.nodes.asMutable(); - version: number = this.tree.version + 1; - private mutations: Map<ImmutableTree.Ref, Node<T>> = new Map(); - - mutate(ref: ImmutableTree.Ref): MutableNode<T> { - return mutateNode(this.nodes, this.mutations, ref); - } - - get rootRef() { return this.tree.rootRef; } - getRef(e: T) { - return this.tree.getRef(e); - } - - getValue(ref: Ref) { - const n = this.nodes.get(ref); - return n ? n.value : void 0; - } - - add(parentRef: ImmutableTree.Ref, value: T) { - const ref = this.getRef(value); - ensureNotPresent(this.nodes, ref); - const parent = this.mutate(parentRef); - const node: Node<T> = { ref, version: 0, value, parent: parent.ref, children: OrderedSet<string>().asMutable() }; - this.mutations.set(ref, node); - parent.children.add(ref); - this.nodes.set(ref, node); - return node; - } - - setValue(ref: ImmutableTree.Ref, value: T): Node<T> { - checkSetRef(ref, this.getRef(value)); - const node = this.mutate(ref); - node.value = value; - return node; - } - - remove<T>(ref: ImmutableTree.Ref): Node<T>[] { - const { nodes, mutations } = this; - const node = nodes.get(ref); - if (!node) return []; - const parent = nodes.get(node.parent)!; - const children = this.mutate(parent.ref).children; - const st = subtreePostOrder(this, node); - if (ref !== this.rootRef) children.delete(ref); - for (const n of st) { - nodes.delete(n.value.ref); - mutations.delete(n.value.ref); - } - return st; - } - - removeChildren(ref: ImmutableTree.Ref): Node<T>[] { - const { nodes, mutations } = this; - let node = nodes.get(ref); - if (!node || !node.children.size) return []; - node = this.mutate(ref); - const st = subtreePostOrder(this, node); - node.children.clear(); - for (const n of st) { - if (n === node) continue; - nodes.delete(n.value.ref); - mutations.delete(n.value.ref); - } - return st; - } - - asImmutable() { - if (this.mutations.size === 0) return this.tree; - - this.mutations.forEach(m => (m as MutableNode<T>).children = m.children.asImmutable()); - return new Impl<T>(this.tree.rootRef, this.nodes.asImmutable(), this.tree.getRef, this.version); - } - - constructor(private tree: ImmutableTree<T>) { - - } - } -} \ No newline at end of file diff --git a/src/mol-task/execution/observable.ts b/src/mol-task/execution/observable.ts index 5b8e82f8991fead4f667dde12a18840ea63ff0d5..5b8d15a07ea63b139192cd74830778c20f4ea79e 100644 --- a/src/mol-task/execution/observable.ts +++ b/src/mol-task/execution/observable.ts @@ -7,7 +7,7 @@ import { Task } from '../task' import { RuntimeContext } from './runtime-context' import { Progress } from './progress' -import { now } from '../util/now' +import { now } from 'mol-util/now'; import { Scheduler } from '../util/scheduler' import { UserTiming } from '../util/user-timing' diff --git a/src/mol-task/index.ts b/src/mol-task/index.ts index 2ef6489c9fffe85d67739274d1aab599dae14de9..62d09e84692b7a2195d41b5295647bdd610d51b7 100644 --- a/src/mol-task/index.ts +++ b/src/mol-task/index.ts @@ -7,9 +7,8 @@ import { Task } from './task' import { RuntimeContext } from './execution/runtime-context' import { Progress } from './execution/progress' -import { now } from './util/now' import { Scheduler } from './util/scheduler' import { MultistepTask } from './util/multistep' import { chunkedSubtask } from './util/chunked' -export { Task, RuntimeContext, Progress, now, Scheduler, MultistepTask, chunkedSubtask } \ No newline at end of file +export { Task, RuntimeContext, Progress, Scheduler, MultistepTask, chunkedSubtask } \ No newline at end of file diff --git a/src/mol-task/util/chunked.ts b/src/mol-task/util/chunked.ts index d091e57066cbda2c53c38d14b0d18a2df53d4c8f..88233dd17a1ded596897308eaf8ae0f31933e5b9 100644 --- a/src/mol-task/util/chunked.ts +++ b/src/mol-task/util/chunked.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { now } from './now' +import { now } from 'mol-util/now'; import { RuntimeContext } from '../execution/runtime-context' type UniformlyChunkedFn<S> = (chunkSize: number, state: S) => number diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts index 08e475e6d5d96cd9594bb764f836c10a18e58c89..67bd8a14ca2e71920f84876edfaead26427fa858 100644 --- a/src/mol-util/input/input-observer.ts +++ b/src/mol-util/input/input-observer.ts @@ -141,6 +141,8 @@ interface InputObserver { pinch: Subject<PinchInput>, click: Subject<ClickInput>, move: Subject<MoveInput>, + leave: Subject<undefined>, + enter: Subject<undefined>, resize: Subject<ResizeInput>, dispose: () => void @@ -175,6 +177,8 @@ namespace InputObserver { const wheel = new Subject<WheelInput>() const pinch = new Subject<PinchInput>() const resize = new Subject<ResizeInput>() + const leave = new Subject<undefined>() + const enter = new Subject<undefined>() attach() @@ -189,6 +193,8 @@ namespace InputObserver { pinch, click, move, + leave, + enter, resize, dispose @@ -204,6 +210,9 @@ namespace InputObserver { window.addEventListener('mousemove', onMouseMove as any, false) window.addEventListener('mouseup', onMouseUp as any, false) + element.addEventListener('mouseenter', onMouseEnter as any, false) + element.addEventListener('mouseleave', onMouseLeave as any, false) + element.addEventListener('touchstart', onTouchStart as any, false) element.addEventListener('touchmove', onTouchMove as any, false) element.addEventListener('touchend', onTouchEnd as any, false) @@ -227,6 +236,9 @@ namespace InputObserver { window.removeEventListener('mousemove', onMouseMove as any, false) window.removeEventListener('mouseup', onMouseUp as any, false) + element.removeEventListener('mouseenter', onMouseEnter as any, false) + element.removeEventListener('mouseleave', onMouseLeave as any, false) + element.removeEventListener('touchstart', onTouchStart as any, false) element.removeEventListener('touchmove', onTouchMove as any, false) element.removeEventListener('touchend', onTouchEnd as any, false) @@ -386,6 +398,14 @@ namespace InputObserver { } } + function onMouseEnter (ev: Event) { + enter.next(); + } + + function onMouseLeave (ev: Event) { + leave.next(); + } + function onResize (ev: Event) { resize.next() } diff --git a/src/mol-util/log-entry.ts b/src/mol-util/log-entry.ts new file mode 100644 index 0000000000000000000000000000000000000000..f54a277a9750b15540f9576c61c2366630553be7 --- /dev/null +++ b/src/mol-util/log-entry.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +export { LogEntry } + +interface LogEntry { + type: LogEntry.Type, + timestamp: Date, + message: string +} + +namespace LogEntry { + export type Type = 'message' | 'error' | 'warning' | 'info' + + export function message(msg: string): LogEntry { return { type: 'message', timestamp: new Date(), message: msg }; } + export function error(msg: string): LogEntry { return { type: 'error', timestamp: new Date(), message: msg }; } + export function warning(msg: string): LogEntry { return { type: 'warning', timestamp: new Date(), message: msg }; } + export function info(msg: string): LogEntry { return { type: 'info', timestamp: new Date(), message: msg }; } +} \ No newline at end of file diff --git a/src/mol-util/memoize.ts b/src/mol-util/memoize.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9427a9c90c6fc94d06fd10b91d788a31b4ccaa8 --- /dev/null +++ b/src/mol-util/memoize.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +export function memoizeOne<Args extends any[], T>(f: (...args: Args) => T): (...args: Args) => T { + let lastArgs: any[] | undefined = void 0, value: any = void 0; + return (...args) => { + if (!lastArgs || lastArgs.length !== args.length) { + lastArgs = args; + value = f.apply(void 0, args); + return value; + } + for (let i = 0, _i = args.length; i < _i; i++) { + if (args[i] !== lastArgs[i]) { + lastArgs = args; + value = f.apply(void 0, args); + return value; + } + } + return value; + } +} \ No newline at end of file diff --git a/src/mol-task/util/now.ts b/src/mol-util/now.ts similarity index 52% rename from src/mol-task/util/now.ts rename to src/mol-util/now.ts index f4961d217bf3570878d2d0f687c1c19a2f11f8b0..c9c9f4f631b9b790340b326591fd0f0e8819d4fd 100644 --- a/src/mol-task/util/now.ts +++ b/src/mol-util/now.ts @@ -7,7 +7,7 @@ declare var process: any; declare var window: any; -const now: () => number = (function () { +const now: () => now.Timestamp = (function () { if (typeof window !== 'undefined' && window.performance) { const perf = window.performance; return () => perf.now(); @@ -23,4 +23,25 @@ const now: () => number = (function () { } }()); -export { now } \ No newline at end of file +namespace now { + export type Timestamp = number & { '@type': 'now-timestamp' } +} + + +function formatTimespan(t: number) { + if (isNaN(t)) return 'n/a'; + + let h = Math.floor(t / (60 * 60 * 1000)), + m = Math.floor(t / (60 * 1000) % 60), + s = Math.floor(t / 1000 % 60), + ms = Math.floor(t % 1000).toString(); + + while (ms.length < 3) ms = '0' + ms; + + if (h > 0) return `${h}h${m}m${s}.${ms}s`; + if (m > 0) return `${m}m${s}.${ms}s`; + if (s > 0) return `${s}.${ms}s`; + return `${t.toFixed(0)}ms`; +} + +export { now, formatTimespan } \ No newline at end of file diff --git a/src/mol-util/performance-monitor.ts b/src/mol-util/performance-monitor.ts index 614b19a29930bb8eda3424b5e7a5f731acb8493c..615bf93ba0aef9689e84cc820f6a2d11f32caf2e 100644 --- a/src/mol-util/performance-monitor.ts +++ b/src/mol-util/performance-monitor.ts @@ -5,7 +5,7 @@ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. */ -import { now } from 'mol-task/util/now' +import { now } from 'mol-util/now'; export class PerformanceMonitor { private starts = new Map<string, number>(); diff --git a/src/mol-util/uuid.ts b/src/mol-util/uuid.ts index 4c77f7de6120c640cb8a025adf32ffae7429f6a5..5ddf5bb22cd9f6e4952fecf699609208d59ec740 100644 --- a/src/mol-util/uuid.ts +++ b/src/mol-util/uuid.ts @@ -4,12 +4,23 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { now } from 'mol-task' +import { now } from 'mol-util/now'; type UUID = string & { '@type': 'uuid' } namespace UUID { - export function create(): UUID { + const chars: string[] = []; + /** Creates 22 characted "base64" UUID */ + export function create22(): UUID { + let d = (+new Date()) + now(); + for (let i = 0; i < 16; i++) { + chars[i] = String.fromCharCode((d + Math.random()*0xff)%0xff | 0); + d = Math.floor(d/0xff); + } + return btoa(chars.join('')).replace(/\+/g, '-').replace(/\//g, '_').substr(0, 22) as UUID; + } + + export function createv4(): UUID { let d = (+new Date()) + now(); const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = (d + Math.random()*16)%16 | 0; diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts index dd594d26c88e747ed8c30b3fa97db41986965ad3..1d9469185f02a7817c6175d2f69f0015c44d96a0 100644 --- a/src/perf-tests/state.ts +++ b/src/perf-tests/state.ts @@ -1,135 +1,133 @@ -import { State, StateObject, StateTree, Transformer, StateSelection } from 'mol-state'; -import { Task } from 'mol-task'; -import * as util from 'util'; - -export type TypeClass = 'root' | 'shape' | 'prop' -export interface ObjProps { label: string } -export interface TypeInfo { name: string, class: TypeClass } - -const _obj = StateObject.factory<TypeInfo, ObjProps>() -const _transform = Transformer.factory('test'); - -export class Root extends _obj({ name: 'Root', class: 'root' }) { } -export class Square extends _obj<{ a: number }>({ name: 'Square', class: 'shape' }) { } -export class Circle extends _obj<{ r: number }>({ name: 'Circle', class: 'shape' }) { } -export class Area extends _obj<{ volume: number }>({ name: 'Area', class: 'prop' }) { } - -export const CreateSquare = _transform<Root, Square, { a: number }>({ - name: 'create-square', - from: [Root], - to: [Square], - apply({ params: p }) { - return new Square({ label: `Square a=${p.a}` }, p); - }, - update({ b, newParams: p }) { - b.props.label = `Square a=${p.a}` - b.data.a = p.a; - return Transformer.UpdateResult.Updated; - } -}); - -export const CreateCircle = _transform<Root, Circle, { r: number }>({ - name: 'create-circle', - from: [Root], - to: [Square], - apply({ params: p }) { - return new Circle({ label: `Circle r=${p.r}` }, p); - }, - update({ b, newParams: p }) { - b.props.label = `Circle r=${p.r}` - b.data.r = p.r; - return Transformer.UpdateResult.Updated; - } -}); - -export const CaclArea = _transform<Square | Circle, Area, {}>({ - name: 'calc-area', - from: [Square, Circle], - to: [Area], - apply({ a }) { - if (a instanceof Square) return new Area({ label: 'Area' }, { volume: a.data.a * a.data.a }); - else if (a instanceof Circle) return new Area({ label: 'Area' }, { volume: a.data.r * a.data.r * Math.PI }); - throw new Error('Unknown object type.'); - }, - update({ a, b }) { - if (a instanceof Square) b.data.volume = a.data.a * a.data.a; - else if (a instanceof Circle) b.data.volume = a.data.r * a.data.r * Math.PI; - else throw new Error('Unknown object type.'); - return Transformer.UpdateResult.Updated; - } -}); - -export async function runTask<A>(t: A | Task<A>): Promise<A> { - if ((t as any).run) return await (t as Task<A>).run(); - return t as A; -} - -function hookEvents(state: State) { - state.context.events.object.created.subscribe(e => console.log('created:', e.ref)); - state.context.events.object.removed.subscribe(e => console.log('removed:', e.ref)); - state.context.events.object.replaced.subscribe(e => console.log('replaced:', e.ref)); - state.context.events.object.stateChanged.subscribe(e => console.log('stateChanged:', e.ref, - StateObject.StateType[state.objects.get(e.ref)!.state])); - state.context.events.object.updated.subscribe(e => console.log('updated:', e.ref)); -} - -export async function testState() { - const state = State.create(new Root({ label: 'Root' }, { })); - hookEvents(state); - - const tree = state.tree; - const builder = StateTree.build(tree); - builder.toRoot<Root>() - .apply(CreateSquare, { a: 10 }, { ref: 'square' }) - .apply(CaclArea); - const tree1 = builder.getTree(); - - printTTree(tree1); - - const tree2 = StateTree.updateParams<typeof CreateSquare>(tree1, 'square', { a: 15 }); - printTTree(tree1); - printTTree(tree2); - - await state.update(tree1).run(); - console.log('----------------'); - console.log(util.inspect(state.objects, true, 3, true)); - - console.log('----------------'); - const jsonString = JSON.stringify(StateTree.toJSON(tree2), null, 2); - const jsonData = JSON.parse(jsonString); - printTTree(tree2); - console.log(jsonString); - const treeFromJson = StateTree.fromJSON(jsonData); - printTTree(treeFromJson); - - console.log('----------------'); - await state.update(treeFromJson).run(); - console.log(util.inspect(state.objects, true, 3, true)); - - console.log('----------------'); - - const q = StateSelection.byRef('square').parent(); - const sel = StateSelection.select(q, state); - console.log(sel); -} - -testState(); - - -//test(); - -export function printTTree(tree: StateTree) { - let lines: string[] = []; - function print(offset: string, ref: any) { - const t = tree.nodes.get(ref)!; - const tr = t.value; - - const name = tr.transformer.id; - lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.value.version}`); - offset += ' '; - - t.children.forEach(c => print(offset, c!)); - } - print('', tree.rootRef); - console.log(lines.join('\n')); -} \ No newline at end of file +// import { State, StateObject, StateTree, Transformer } from 'mol-state'; +// import { Task } from 'mol-task'; +// import * as util from 'util'; + +// export type TypeClass = 'root' | 'shape' | 'prop' +// export interface ObjProps { label: string } +// export interface TypeInfo { name: string, class: TypeClass } + +// const _obj = StateObject.factory<TypeInfo, ObjProps>() +// const _transform = Transformer.factory('test'); + +// export class Root extends _obj({ name: 'Root', class: 'root' }) { } +// export class Square extends _obj<{ a: number }>({ name: 'Square', class: 'shape' }) { } +// export class Circle extends _obj<{ r: number }>({ name: 'Circle', class: 'shape' }) { } +// export class Area extends _obj<{ volume: number }>({ name: 'Area', class: 'prop' }) { } + +// export const CreateSquare = _transform<Root, Square, { a: number }>({ +// name: 'create-square', +// from: [Root], +// to: [Square], +// apply({ params: p }) { +// return new Square({ label: `Square a=${p.a}` }, p); +// }, +// update({ b, newParams: p }) { +// b.props.label = `Square a=${p.a}` +// b.data.a = p.a; +// return Transformer.UpdateResult.Updated; +// } +// }); + +// export const CreateCircle = _transform<Root, Circle, { r: number }>({ +// name: 'create-circle', +// from: [Root], +// to: [Square], +// apply({ params: p }) { +// return new Circle({ label: `Circle r=${p.r}` }, p); +// }, +// update({ b, newParams: p }) { +// b.props.label = `Circle r=${p.r}` +// b.data.r = p.r; +// return Transformer.UpdateResult.Updated; +// } +// }); + +// export const CaclArea = _transform<Square | Circle, Area, {}>({ +// name: 'calc-area', +// from: [Square, Circle], +// to: [Area], +// apply({ a }) { +// if (a instanceof Square) return new Area({ label: 'Area' }, { volume: a.data.a * a.data.a }); +// else if (a instanceof Circle) return new Area({ label: 'Area' }, { volume: a.data.r * a.data.r * Math.PI }); +// throw new Error('Unknown object type.'); +// }, +// update({ a, b }) { +// if (a instanceof Square) b.data.volume = a.data.a * a.data.a; +// else if (a instanceof Circle) b.data.volume = a.data.r * a.data.r * Math.PI; +// else throw new Error('Unknown object type.'); +// return Transformer.UpdateResult.Updated; +// } +// }); + +// export async function runTask<A>(t: A | Task<A>): Promise<A> { +// if ((t as any).run) return await (t as Task<A>).run(); +// return t as A; +// } + +// function hookEvents(state: State) { +// state.events.object.created.subscribe(e => console.log('created:', e.ref)); +// state.events.object.removed.subscribe(e => console.log('removed:', e.ref)); +// state.events.object.replaced.subscribe(e => console.log('replaced:', e.ref)); +// state.events.object.cellState.subscribe(e => console.log('stateChanged:', e.ref, e.cell.status)); +// state.events.object.updated.subscribe(e => console.log('updated:', e.ref)); +// } + +// export async function testState() { +// const state = State.create(new Root({ label: 'Root' }, { })); +// hookEvents(state); + +// const tree = state.tree; +// const builder = tree.build(); +// builder.toRoot<Root>() +// .apply(CreateSquare, { a: 10 }, { ref: 'square' }) +// .apply(CaclArea); +// const tree1 = builder.getTree(); + +// printTTree(tree1); + +// const tree2 = StateTree.updateParams<typeof CreateSquare>(tree1, 'square', { a: 15 }); +// printTTree(tree1); +// printTTree(tree2); + +// await state.update(tree1).run(); +// console.log('----------------'); +// console.log(util.inspect(state.cells, true, 3, true)); + +// console.log('----------------'); +// const jsonString = JSON.stringify(StateTree.toJSON(tree2), null, 2); +// const jsonData = JSON.parse(jsonString); +// printTTree(tree2); +// console.log(jsonString); +// const treeFromJson = StateTree.fromJSON(jsonData); +// printTTree(treeFromJson); + +// console.log('----------------'); +// await state.update(treeFromJson).run(); +// console.log(util.inspect(state.cells, true, 3, true)); + +// console.log('----------------'); + +// const sel = state.select('square'); +// console.log(sel); +// } + +// testState(); + + +// //test(); + +// export function printTTree(tree: StateTree) { +// let lines: string[] = []; +// function print(offset: string, ref: any) { +// const t = tree.nodes.get(ref)!; +// const tr = t; + +// const name = tr.transformer.id; +// lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.version}`); +// offset += ' '; + +// tree.children.get(ref).forEach(c => print(offset, c!)); +// } +// print('', tree.root.ref); +// console.log(lines.join('\n')); +// } \ No newline at end of file diff --git a/src/perf-tests/tasks.ts b/src/perf-tests/tasks.ts index f0c0eb86b3e5948fa3cd061f4bb0c697f861b0ee..068c47b5202691331adfb598924dd363ed798deb 100644 --- a/src/perf-tests/tasks.ts +++ b/src/perf-tests/tasks.ts @@ -1,5 +1,5 @@ import * as B from 'benchmark' -import { now } from 'mol-task/util/now' +import { now } from 'mol-util/now'; import { Scheduler } from 'mol-task/util/scheduler' export namespace Tasks { diff --git a/src/servers/model/preprocess/parallel.ts b/src/servers/model/preprocess/parallel.ts index 2e24f3e0ceb370394aec5990e6e2f20d5a3b2fef..1b600b5b031313b63827de07d5a13992e541974d 100644 --- a/src/servers/model/preprocess/parallel.ts +++ b/src/servers/model/preprocess/parallel.ts @@ -6,7 +6,7 @@ import * as path from 'path' import * as cluster from 'cluster' -import { now } from 'mol-task'; +import { now } from 'mol-util/now'; import { PerformanceMonitor } from 'mol-util/performance-monitor'; import { preprocessFile } from './preprocess'; import { createModelPropertiesProvider } from '../property-provider'; diff --git a/src/servers/model/properties/providers/pdbe.ts b/src/servers/model/properties/providers/pdbe.ts index df2b852b4a029c0ec9dd1d2be41b6f3cdbb9e56b..66fe25ef91c515909eb6ffc67af3b44df7f7560c 100644 --- a/src/servers/model/properties/providers/pdbe.ts +++ b/src/servers/model/properties/providers/pdbe.ts @@ -83,7 +83,7 @@ function getParam<T>(params: any, ...path: string[]): T | undefined { function apiQueryProvider(urlPrefix: string, cache: any) { - const cacheKey = UUID.create(); + const cacheKey = UUID.create22(); return async (model: Model) => { try { if (cache[cacheKey]) return cache[cacheKey]; diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts index f1d5e2f1c213690c586d518208d58d0231aac139..83fe022eb141e6fbf4ddeeaa30d15781978b19a4 100644 --- a/src/servers/model/server/api-local.ts +++ b/src/servers/model/server/api-local.ts @@ -10,7 +10,7 @@ import { JobManager, Job } from './jobs'; import { ConsoleLogger } from 'mol-util/console-logger'; import { resolveJob } from './query'; import { StructureCache } from './structure-wrapper'; -import { now } from 'mol-task'; +import { now } from 'mol-util/now'; import { PerformanceMonitor } from 'mol-util/performance-monitor'; import { QueryName } from './api'; diff --git a/src/servers/model/server/jobs.ts b/src/servers/model/server/jobs.ts index 7697d30c52260acf5d6ba729eb2c6b4a63b3d048..f1e408174af7ac66d2dc79610df48d3b95592e7b 100644 --- a/src/servers/model/server/jobs.ts +++ b/src/servers/model/server/jobs.ts @@ -43,7 +43,7 @@ export function createJob<Name extends QueryName>(definition: JobDefinition<Name const normalizedParams = normalizeQueryParams(queryDefinition, definition.queryParams); const sourceId = definition.sourceId || '_local_'; return { - id: UUID.create(), + id: UUID.create22(), datetime_utc: `${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')}`, key: `${sourceId}/${definition.entryId}`, sourceId, diff --git a/src/servers/model/server/query.ts b/src/servers/model/server/query.ts index 0c80ae56ccf240290d028d7424dabd16dc7a86ad..f1079596487273caec14432f1fbbc0afc7274063 100644 --- a/src/servers/model/server/query.ts +++ b/src/servers/model/server/query.ts @@ -8,7 +8,8 @@ import { Column } from 'mol-data/db'; import { CifWriter } from 'mol-io/writer/cif'; import { StructureQuery, StructureSelection, Structure } from 'mol-model/structure'; import { encode_mmCIF_categories } from 'mol-model/structure/export/mmcif'; -import { now, Progress } from 'mol-task'; +import { Progress } from 'mol-task'; +import { now } from 'mol-util/now'; import { ConsoleLogger } from 'mol-util/console-logger'; import { PerformanceMonitor } from 'mol-util/performance-monitor'; import Config from '../config'; diff --git a/src/servers/model/utils/fetch-props-pdbe.ts b/src/servers/model/utils/fetch-props-pdbe.ts index f28d9946b62a9f111a30184649298e4042349fd9..76d77f662c7b799ccd6cea58a1143a3880be40e2 100644 --- a/src/servers/model/utils/fetch-props-pdbe.ts +++ b/src/servers/model/utils/fetch-props-pdbe.ts @@ -9,7 +9,7 @@ import * as fs from 'fs' import * as path from 'path' import * as argparse from 'argparse' import { makeDir } from 'mol-util/make-dir'; -import { now } from 'mol-task'; +import { now } from 'mol-util/now'; import { PerformanceMonitor } from 'mol-util/performance-monitor'; const cmdParser = new argparse.ArgumentParser({ diff --git a/src/servers/volume/server/query/execute.ts b/src/servers/volume/server/query/execute.ts index d6ecf6ecf675b481cb27569668cb33d3bd2cf782..c5d4272dfdbb87975ec80759d5abab86e4cdf6cf 100644 --- a/src/servers/volume/server/query/execute.ts +++ b/src/servers/volume/server/query/execute.ts @@ -26,7 +26,7 @@ export default async function execute(params: Data.QueryParams, outputProvider: const start = getTime(); State.pendingQueries++; - const guid = UUID.create() as any as string; + const guid = UUID.create22() as any as string; params.detail = Math.min(Math.max(0, params.detail | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1); ConsoleLogger.logId(guid, 'Info', `id=${params.sourceId},encoding=${params.asBinary ? 'binary' : 'text'},detail=${params.detail},${queryBoxToString(params.box)}`);