Browse Source

async gl repr object handling

Alexander Rose 5 years ago
parent
commit
991d2e3a57

+ 33 - 10
src/mol-canvas3d/canvas3d.ts

@@ -31,6 +31,7 @@ import { PixelData } from '../mol-util/image';
 import { readTexture } from '../mol-gl/compute/util';
 import { DrawPass } from './passes/draw';
 import { PickPass } from './passes/pick';
+import { Task } from '../mol-task';
 
 export const Canvas3DParams = {
     // TODO: FPS cap?
@@ -66,6 +67,7 @@ interface Canvas3D {
     getLoci: (pickingId: PickingId) => Representation.Loci
 
     readonly didDraw: BehaviorSubject<now.Timestamp>
+    readonly reprCount: BehaviorSubject<number>
 
     handleResize: () => void
     /** Focuses camera on scene's bounding sphere, centered and zoomed. */
@@ -85,12 +87,13 @@ interface Canvas3D {
 }
 
 const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()))
+const DefaultRunTask = (task: Task<unknown>) => task.run()
 
 namespace Canvas3D {
     export interface HighlightEvent { current: Representation.Loci, modifiers?: ModifiersKeys }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
 
-    export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}) {
+    export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
         const gl = getGLContext(canvas, {
             alpha: false,
             antialias: true,
@@ -99,10 +102,10 @@ namespace Canvas3D {
         })
         if (gl === null) throw new Error('Could not create a WebGL rendering context')
         const input = InputObserver.fromElement(canvas)
-        return Canvas3D.create(gl, input, props)
+        return Canvas3D.create(gl, input, props, runTask)
     }
 
-    export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}): Canvas3D {
+    export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
         const p = { ...PD.getDefaultValues(Canvas3DParams), ...props }
 
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
@@ -137,6 +140,7 @@ namespace Canvas3D {
 
         let isUpdating = false
         let drawPending = false
+        let cameraResetRequested = false
 
         function getLoci(pickingId: PickingId) {
             let loci: Loci = EmptyLoci
@@ -201,7 +205,7 @@ namespace Canvas3D {
         }
 
         function render(variant: 'pick' | 'draw', force: boolean) {
-            if (isUpdating) return false
+            if (isUpdating || scene.isCommiting) return false
 
             let didRender = false
             controls.update(currentTime);
@@ -279,8 +283,15 @@ namespace Canvas3D {
             scene.update(repr.renderObjects, false)
             if (debugHelper.isEnabled) debugHelper.update()
             isUpdating = false
-            requestDraw(true)
-            reprCount.next(reprRenderObjects.size)
+
+            runTask(scene.commit()).then(() => {
+                if (cameraResetRequested && !scene.isCommiting) {
+                    camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
+                    cameraResetRequested = false
+                }
+                requestDraw(true)
+                reprCount.next(reprRenderObjects.size)
+            })
         }
 
         handleResize()
@@ -307,8 +318,15 @@ namespace Canvas3D {
                     scene.update(void 0, false)
                     if (debugHelper.isEnabled) debugHelper.update()
                     isUpdating = false
-                    requestDraw(true)
-                    reprCount.next(reprRenderObjects.size)
+
+                    runTask(scene.commit()).then(() => {
+                        if (cameraResetRequested && !scene.isCommiting) {
+                            camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
+                            cameraResetRequested = false
+                        }
+                        requestDraw(true)
+                        reprCount.next(reprRenderObjects.size)
+                    })
                 }
             },
             update: (repr, keepSphere) => {
@@ -334,8 +352,12 @@ namespace Canvas3D {
 
             handleResize,
             resetCamera: () => {
-                camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
-                requestDraw(true);
+                if (scene.isCommiting) {
+                    cameraResetRequested = true
+                } else {
+                    camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
+                    requestDraw(true);
+                }
             },
             camera,
             downloadScreenshot: () => {
@@ -351,6 +373,7 @@ namespace Canvas3D {
                 }
             },
             didDraw,
+            reprCount,
             setProps: (props: Partial<Canvas3DProps>) => {
                 if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
                     camera.setState({ mode: props.cameraMode })

+ 74 - 21
src/mol-gl/scene.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,6 +12,8 @@ import { Object3D } from './object3d';
 import { Sphere3D } from '../mol-math/geometry';
 import { Vec3 } from '../mol-math/linear-algebra';
 import { BoundaryHelper } from '../mol-math/geometry/boundary-helper';
+import { RuntimeContext, Task } from '../mol-task';
+import { AsyncQueue } from '../mol-util/async-queue';
 
 const boundaryHelper = new BoundaryHelper();
 function calculateBoundingSphere(renderables: Renderable<RenderableValues & BaseValues>[], boundingSphere: Sphere3D): Sphere3D {
@@ -54,10 +56,12 @@ interface Scene extends Object3D {
     readonly count: number
     readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>>
     readonly boundingSphere: Sphere3D
+    readonly isCommiting: boolean
 
     update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void
-    add: (o: GraphicsRenderObject) => Renderable<any>
+    add: (o: GraphicsRenderObject) => void // Renderable<any>
     remove: (o: GraphicsRenderObject) => void
+    commit: () => Task<void>
     has: (o: GraphicsRenderObject) => boolean
     clear: () => void
     forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: GraphicsRenderObject) => void) => void
@@ -68,15 +72,63 @@ namespace Scene {
         const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>()
         const renderables: Renderable<RenderableValues & BaseValues>[] = []
         const boundingSphere = Sphere3D.zero()
+
         let boundingSphereDirty = true
 
         const object3d = Object3D.create()
 
+        const add = (o: GraphicsRenderObject) => {
+            if (!renderableMap.has(o)) {
+                const renderable = createRenderable(ctx, o)
+                renderables.push(renderable)
+                renderableMap.set(o, renderable)
+                boundingSphereDirty = true
+                return renderable;
+            } else {
+                console.warn(`RenderObject with id '${o.id}' already present`)
+                return renderableMap.get(o)!
+            }
+        }
+
+        const remove = (o: GraphicsRenderObject) => {
+            const renderable = renderableMap.get(o)
+            if (renderable) {
+                renderable.dispose()
+                renderables.splice(renderables.indexOf(renderable), 1)
+                renderableMap.delete(o)
+                boundingSphereDirty = true
+            }
+        }
+
+        const commitQueue = new AsyncQueue<any>();
+        const toAdd: GraphicsRenderObject[] = []
+        const toRemove: GraphicsRenderObject[] = []
+
+        type CommitParams = { toAdd: GraphicsRenderObject[], toRemove: GraphicsRenderObject[] }
+
+        const step = 100
+        const handle = async (ctx: RuntimeContext, arr: GraphicsRenderObject[], fn: (o: GraphicsRenderObject) => void, message: string) => {
+            for (let i = 0, il = arr.length; i < il; i += step) {
+                if (ctx.shouldUpdate) await ctx.update({ message, current: i, max: il })
+                for (let j = i, jl = Math.min(i + step, il); j < jl; ++j) {
+                    fn(arr[j])
+                }
+            }
+        }
+
+        const commit = async (ctx: RuntimeContext, p: CommitParams) => {
+            await handle(ctx, p.toRemove, remove, 'Removing GraphicsRenderObjects')
+            await handle(ctx, p.toAdd, add, 'Adding GraphicsRenderObjects')
+            if (ctx.shouldUpdate) await ctx.update({ message: 'Sorting GraphicsRenderObjects' })
+            renderables.sort(renderableSort)
+        }
+
         return {
             get view () { return object3d.view },
             get position () { return object3d.position },
             get direction () { return object3d.direction },
             get up () { return object3d.up },
+            get isCommiting () { return commitQueue.length > 0 },
 
             update(objects, keepBoundingSphere) {
                 Object3D.update(object3d)
@@ -94,27 +146,28 @@ namespace Scene {
                 if (!keepBoundingSphere) boundingSphereDirty = true
             },
             add: (o: GraphicsRenderObject) => {
-                if (!renderableMap.has(o)) {
-                    const renderable = createRenderable(ctx, o)
-                    renderables.push(renderable)
-                    renderables.sort(renderableSort)
-                    renderableMap.set(o, renderable)
-                    boundingSphereDirty = true
-                    return renderable;
-                } else {
-                    console.warn(`RenderObject with id '${o.id}' already present`)
-                    return renderableMap.get(o)!
-                }
+                toAdd.push(o)
             },
             remove: (o: GraphicsRenderObject) => {
-                const renderable = renderableMap.get(o)
-                if (renderable) {
-                    renderable.dispose()
-                    renderables.splice(renderables.indexOf(renderable), 1)
-                    renderables.sort(renderableSort)
-                    renderableMap.delete(o)
-                    boundingSphereDirty = true
-                }
+                toRemove.push(o)
+            },
+            commit: () => {
+                const params = { toAdd: [ ...toAdd ], toRemove: [ ...toRemove ] }
+                toAdd.length = 0
+                toRemove.length = 0
+
+                return Task.create('Commiting GraphicsRenderObjects', async ctx => {
+                    const removed = await commitQueue.enqueue(params);
+                    if (!removed) return;
+
+                    try {
+                        await commit(ctx, params);
+                    } finally {
+                        commitQueue.handled(params);
+                    }
+                }, () => {
+                    commitQueue.remove(params);
+                })
             },
             has: (o: GraphicsRenderObject) => {
                 return renderableMap.has(o)

+ 7 - 4
src/mol-plugin/behavior/static/representation.ts

@@ -18,15 +18,19 @@ export function registerDefault(ctx: PluginContext) {
 export function SyncRepresentationToCanvas(ctx: PluginContext) {
     let reprCount = 0;
 
+    ctx.events.canvas3d.initialized.subscribe(() => {
+        ctx.canvas3d.reprCount.subscribe(v => {
+            if (reprCount === 0) ctx.canvas3d.resetCamera();
+            reprCount = v;
+        });
+    })
+
     const events = ctx.state.dataState.events;
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
         updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         e.obj.data.repr.setState({ syncManually: true });
         ctx.canvas3d.add(e.obj.data.repr);
-
-        if (reprCount === 0) ctx.canvas3d.resetCamera();
-        reprCount++;
     });
     events.object.updated.subscribe(e => {
         if (e.oldObj && SO.isRepresentation3D(e.oldObj)) {
@@ -50,7 +54,6 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
         ctx.canvas3d.remove(e.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
         e.obj.data.repr.destroy();
-        reprCount--;
     });
 }
 

+ 4 - 1
src/mol-plugin/context.ts

@@ -73,6 +73,7 @@ export class PluginContext {
         log: this.ev<LogEntry>(),
         task: this.tasks.events,
         canvas3d: {
+            initialized: this.ev(),
             settingsUpdated: this.ev()
         },
         interactivity: {
@@ -127,7 +128,9 @@ export class PluginContext {
         try {
             this.layout.setRoot(container);
             if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial);
-            (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas);
+            (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas, {}, t => this.runTask(t));
+            this.events.canvas3d.initialized.next()
+            this.events.canvas3d.initialized.isStopped = true // TODO is this a good way?
             const renderer = this.canvas3d.props.renderer;
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { renderer: { ...renderer, backgroundColor: Color(0xFCFBF9) } } });
             this.canvas3d.animate();

+ 2 - 0
src/mol-util/async-queue.ts

@@ -11,6 +11,8 @@ export class AsyncQueue<T> {
     private queue: T[] = [];
     private signal = new Subject<{ v: T, stillPresent: boolean }>();
 
+    get length() { return this.queue.length }
+
     enqueue(v: T) {
         this.queue.push(v);
         if (this.queue.length === 1) return true;