Browse Source

refactored volume representations

Alexander Rose 6 years ago
parent
commit
f45b998424

+ 3 - 2
src/apps/structure-info/volume.ts

@@ -15,7 +15,8 @@ import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-se
 import { Table } from 'mol-data/db';
 import { StringBuilder } from 'mol-util';
 import { Task } from 'mol-task';
-import { createVolumeIsosurface } from 'mol-repr/volume/isosurface-mesh';
+import { createVolumeIsosurfaceMesh } from 'mol-repr/volume/isosurface-mesh';
+import { createEmptyTheme } from 'mol-theme/theme';
 
 require('util.promisify').shim();
 const writeFileAsync = util.promisify(fs.writeFile);
@@ -38,7 +39,7 @@ function print(data: Volume) {
 }
 
 async function doMesh(data: Volume, filename: string) {
-    const mesh = await Task.create('', runtime => createVolumeIsosurface({ runtime }, data.volume, { isoValue: VolumeIsoValue.calcAbsolute(data.volume.dataStats, 1.5) } )).run();
+    const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, data.volume, createEmptyTheme(), { isoValue: VolumeIsoValue.calcAbsolute(data.volume.dataStats, 1.5) } )).run();
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
 
     // Export the mesh in OBJ format.

+ 4 - 0
src/mol-model/volume/data.ts

@@ -35,6 +35,10 @@ namespace VolumeData {
         const translate = Mat4.fromTranslation(_translate, volume.fractionalBox.min);
         return Mat4.mul3(Mat4.zero(), volume.cell.fromFractional, translate, scale);
     }
+
+    export function areEquivalent(volA: VolumeData, volB: VolumeData) {
+        return volA === volB
+    }
 }
 
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative

+ 6 - 14
src/mol-repr/volume/direct-volume.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>
  */
@@ -7,7 +7,6 @@
 import { VolumeData } from 'mol-model/volume'
 import { RuntimeContext } from 'mol-task'
 import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
-import { createRenderObject } from 'mol-gl/render-object';
 import { EmptyLoci } from 'mol-model/loci';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
@@ -15,13 +14,13 @@ import { Box3D } from 'mol-math/geometry';
 import { WebGLContext } from 'mol-gl/webgl/context';
 import { createTexture } from 'mol-gl/webgl/texture';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { BaseGeometry } from 'mol-geo/geometry/base';
 import { VisualUpdateState } from 'mol-repr/util';
 import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
 import { VisualContext } from 'mol-repr/visual';
+import { NullLocation } from 'mol-model/location';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
     const bbox = Box3D.empty()
@@ -144,7 +143,7 @@ export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, v
 
 //
 
-export async function createDirectVolume(ctx: VisualContext, volume: VolumeData, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
+export async function createDirectVolume(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
     const { runtime, webgl } = ctx
     if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in props')
 
@@ -166,22 +165,15 @@ export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: VolumeD
 }
 
 export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> {
-    return VolumeVisual<DirectVolumeParams>({
+    return VolumeVisual<DirectVolume, DirectVolumeParams>({
         defaultProps: PD.getDefaultValues(DirectVolumeParams),
         createGeometry: createDirectVolume,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
         getLoci: () => EmptyLoci,
         mark: () => false,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
         },
-        createRenderObject: (geometry: DirectVolume, locationIt: LocationIterator, theme: Theme, props: PD.Values<DirectVolumeParams>) => {
-            const transform = createIdentityTransform()
-            const values = DirectVolume.Utils.createValues(geometry, transform, locationIt, theme, props)
-            const state = DirectVolume.Utils.createRenderableState(props)
-            return createRenderObject('direct-volume', values, state)
-        },
-        updateValues: DirectVolume.Utils.updateValues,
-        updateBoundingSphere: DirectVolume.Utils.updateBoundingSphere,
-        updateRenderableState: DirectVolume.Utils.updateRenderableState
+        geometryUtils: DirectVolume.Utils
     })
 }
 

+ 17 - 25
src/mol-repr/volume/isosurface-mesh.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 David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -7,23 +7,22 @@
 
 import { VolumeData } from 'mol-model/volume'
 import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
-import { createRenderObject } from 'mol-gl/render-object';
 import { EmptyLoci } from 'mol-model/loci';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { VisualUpdateState } from 'mol-repr/util';
 import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
 import { VisualContext } from 'mol-repr/visual';
+import { NullLocation } from 'mol-model/location';
 
 interface VolumeIsosurfaceProps {
     isoValue: number
 }
 
-export async function createVolumeIsosurface(ctx: VisualContext, volume: VolumeData, props: VolumeIsosurfaceProps, mesh?: Mesh) {
+export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: VolumeData, theme: Theme, props: VolumeIsosurfaceProps, mesh?: Mesh) {
     ctx.runtime.update({ message: 'Marching cubes...' });
 
     const surface = await computeMarchingCubesMesh({
@@ -39,46 +38,39 @@ export async function createVolumeIsosurface(ctx: VisualContext, volume: VolumeD
     return surface;
 }
 
-export const IsosurfaceParams = {
+export const IsosurfaceMeshParams = {
     ...Mesh.Params,
     isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
 }
-export type IsosurfaceParams = typeof IsosurfaceParams
+export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
 export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
-    return PD.clone(IsosurfaceParams)
+    return PD.clone(IsosurfaceMeshParams)
 }
 
-export function IsosurfaceVisual(): VolumeVisual<IsosurfaceParams> {
-    return VolumeVisual<IsosurfaceParams>({
-        defaultProps: PD.getDefaultValues(IsosurfaceParams),
-        createGeometry: createVolumeIsosurface,
+export function IsosurfaceVisual(): VolumeVisual<IsosurfaceMeshParams> {
+    return VolumeVisual<Mesh, IsosurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
+        createGeometry: createVolumeIsosurfaceMesh,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
         getLoci: () => EmptyLoci,
         mark: () => false,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IsosurfaceParams>, currentProps: PD.Values<IsosurfaceParams>) => {
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
             if (newProps.isoValue !== currentProps.isoValue) state.createGeometry = true
         },
-        createRenderObject: (geometry: Mesh, locationIt: LocationIterator, theme: Theme, props: PD.Values<IsosurfaceParams>) => {
-            const transform = createIdentityTransform()
-            const values = Mesh.Utils.createValues(geometry, transform, locationIt, theme, props)
-            const state = Mesh.Utils.createRenderableState(props)
-            return createRenderObject('mesh', values, state)
-        },
-        updateValues: Mesh.Utils.updateValues,
-        updateBoundingSphere: Mesh.Utils.updateBoundingSphere,
-        updateRenderableState: Mesh.Utils.updateRenderableState
+        geometryUtils: Mesh.Utils
     })
 }
 
-export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceParams>): VolumeRepresentation<IsosurfaceParams> {
-    return VolumeRepresentation('Isosurface', ctx, getParams, IsosurfaceVisual)
+export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceMeshParams>): VolumeRepresentation<IsosurfaceMeshParams> {
+    return VolumeRepresentation('Isosurface ', ctx, getParams, IsosurfaceVisual)
 }
 
-export const IsosurfaceRepresentationProvider: VolumeRepresentationProvider<IsosurfaceParams> = {
+export const IsosurfaceRepresentationProvider: VolumeRepresentationProvider<IsosurfaceMeshParams> = {
     label: 'Isosurface',
     description: 'Displays an isosurface of volumetric data.',
     factory: IsosurfaceRepresentation,
     getParams: getIsosurfaceParams,
-    defaultValues: PD.getDefaultValues(IsosurfaceParams),
+    defaultValues: PD.getDefaultValues(IsosurfaceMeshParams),
     defaultColorTheme: 'uniform',
     defaultSizeTheme: 'uniform'
 }

+ 108 - 63
src/mol-repr/volume/representation.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>
  */
@@ -9,94 +9,152 @@ import { Representation, RepresentationContext, RepresentationProvider, Represen
 import { Visual, VisualContext } from '../visual';
 import { VolumeData } from 'mol-model/volume';
 import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
-import { Geometry } from 'mol-geo/geometry/geometry';
+import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data';
-import { GraphicsRenderObject } from 'mol-gl/render-object';
+import { GraphicsRenderObject, createRenderObject } from 'mol-gl/render-object';
 import { Interval } from 'mol-data/int';
-import { RenderableValues } from 'mol-gl/renderable/schema';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { NullLocation } from 'mol-model/location';
 import { VisualUpdateState } from 'mol-repr/util';
 import { ValueCell } from 'mol-util';
 import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
-import { RenderableState } from 'mol-gl/renderable';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { BaseGeometry } from 'mol-geo/geometry/base';
+import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
+import { ColorTheme } from 'mol-theme/color';
+import { createColors } from 'mol-geo/geometry/color-data';
+import { createSizes } from 'mol-geo/geometry/size-data';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { }
 
+function createVolumeRenderObject<G extends Geometry>(volume: VolumeData, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) {
+    const { createValues, createRenderableState } = Geometry.getUtils(geometry)
+    const transform = createIdentityTransform()
+    const values = createValues(geometry, transform, locationIt, theme, props)
+    const state = createRenderableState(props)
+    return createRenderObject(geometry.kind, values, state)
+}
+
 interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
     defaultProps: PD.Values<P>
-    createGeometry(ctx: VisualContext, volumeData: VolumeData, props: PD.Values<P>, geometry?: G): Promise<G>
+    createGeometry(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
+    createLocationIterator(volume: VolumeData): LocationIterator
     getLoci(pickingId: PickingId, id: number): Loci
     mark(loci: Loci, apply: (interval: Interval) => boolean): boolean
-    setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>): void
+    setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
 }
 
 interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry> extends VolumeVisualBuilder<P, G> {
-    createRenderObject(geometry: G, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): GraphicsRenderObject
-    updateValues(values: RenderableValues, newProps: PD.Values<P>): void,
-    updateBoundingSphere(values: RenderableValues, geometry: G): void
-    updateRenderableState(state: RenderableState, props: PD.Values<P>): void
+    geometryUtils: GeometryUtils<G>
 }
 
-export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeometryBuilder<P, Geometry>): VolumeVisual<P> {
-    const { defaultProps, createGeometry, getLoci, mark, setUpdateState } = builder
-    const { createRenderObject, updateValues, updateBoundingSphere, updateRenderableState } = builder
+export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>): VolumeVisual<P> {
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
+    const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
 
-    let currentProps: PD.Values<P>
     let renderObject: GraphicsRenderObject | undefined
+
+    let newProps: PD.Values<P>
+    let newTheme: Theme
+    let newVolume: VolumeData
+
+    let currentProps: PD.Values<P> = Object.assign({}, defaultProps)
+    let currentTheme: Theme = createEmptyTheme()
     let currentVolume: VolumeData
-    let geometry: Geometry
+
+    let geometry: G
     let locationIt: LocationIterator
 
-    async function create(ctx: VisualContext, volume: VolumeData, theme: Theme, props: Partial<PD.Values<P>> = {}) {
-        currentProps = Object.assign({}, defaultProps, props)
-        geometry = await createGeometry(ctx, volume, currentProps, geometry)
-        locationIt = LocationIterator(1, 1, () => NullLocation)
-        renderObject = createRenderObject(geometry, locationIt, theme, currentProps)
-    }
+    function prepareUpdate(theme: Theme, props: Partial<PD.Values<P>>, volume: VolumeData) {
+        if (!volume && !currentVolume) {
+            throw new Error('missing volume')
+        }
 
-    async function update(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}) {
-        if (!renderObject) return
-        const newProps = Object.assign({}, currentProps, props)
+        newProps = Object.assign({}, currentProps, props)
+        newTheme = theme
+        newVolume = volume
 
         VisualUpdateState.reset(updateState)
-        setUpdateState(updateState, newProps, currentProps)
+
+        if (!renderObject) {
+            updateState.createNew = true
+        } else if (!currentVolume || !VolumeData.areEquivalent(newVolume, currentVolume)) {
+            updateState.createNew = true
+        }
+
+        if (updateState.createNew) {
+            updateState.createGeometry = true
+            return
+        }
+
+        setUpdateState(updateState, newProps, currentProps, newTheme, currentTheme)
+
+        if (!ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true
 
         if (updateState.createGeometry) {
-            geometry = await createGeometry(ctx, currentVolume, currentProps, geometry)
-            ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry))
-            updateBoundingSphere(renderObject.values, geometry)
+            updateState.updateColor = true
         }
+    }
 
-        updateValues(renderObject.values, newProps)
-        updateRenderableState(renderObject.state, newProps)
+    function update(newGeometry?: G) {
+        if (updateState.createNew) {
+            locationIt = createLocationIterator(newVolume)
+            if (newGeometry) {
+                renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps)
+            } else {
+                throw new Error('expected geometry to be given')
+            }
+        } else {
+            if (!renderObject) {
+                throw new Error('expected renderObject to be available')
+            }
+
+            locationIt.reset()
+
+            if (updateState.createGeometry) {
+                if (newGeometry) {
+                    ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(newGeometry))
+                    updateBoundingSphere(renderObject.values, newGeometry)
+                } else {
+                    throw new Error('expected geometry to be given')
+                }
+            }
+
+            if (updateState.updateSize) {
+                // not all geometries have size data, so check here
+                if ('uSize' in renderObject.values) {
+                    createSizes(locationIt, newTheme.size, renderObject.values)
+                }
+            }
+
+            if (updateState.updateColor) {
+                createColors(locationIt, newTheme.color, renderObject.values)
+            }
+
+            updateValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+        }
 
         currentProps = newProps
+        currentTheme = newTheme
+        currentVolume = newVolume
+        if (newGeometry) geometry = newGeometry
     }
 
     return {
         get groupCount() { return locationIt ? locationIt.count : 0 },
         get renderObject () { return renderObject },
         async createOrUpdate(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}, volume?: VolumeData) {
-            if (!volume && !currentVolume) {
-                throw new Error('missing volume')
-            } else if (volume && (!currentVolume || !renderObject)) {
-                currentVolume = volume
-                await create(ctx, volume, theme, props)
-            } else if (volume && volume !== currentVolume) {
-                currentVolume = volume
-                await create(ctx, volume, theme, props)
+            prepareUpdate(theme, props, volume || currentVolume)
+            if (updateState.createGeometry) {
+                const newGeometry = createGeometry(ctx, newVolume, newTheme, newProps, geometry)
+                return newGeometry instanceof Promise ? newGeometry.then(update) : update(newGeometry)
             } else {
-                await update(ctx, theme, props)
+                update()
             }
-
-            currentProps = Object.assign({}, defaultProps, props)
         },
         getLoci(pickingId: PickingId) {
             return renderObject ? getLoci(pickingId, renderObject.id) : EmptyLoci
@@ -151,18 +209,17 @@ export const VolumeParams = {
 }
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: () => VolumeVisual<P>): VolumeRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
     const renderObjects: GraphicsRenderObject[] = []
     const _state = Representation.createState()
-    let visual: VolumeVisual<P>
+    let visual: VolumeVisual<P> | undefined
 
     let _volume: VolumeData
-    let _props: PD.Values<P>
     let _params: P
+    let _props: PD.Values<P>
     let _theme = createEmptyTheme()
-    let busy = false
 
     function createOrUpdate(props: Partial<PD.Values<P>> = {}, volume?: VolumeData) {
         if (volume && volume !== _volume) {
@@ -172,22 +229,10 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         }
         _props = Object.assign({}, _props, props)
 
-        return Task.create('VolumeRepresentation.create', async runtime => {
-            // TODO queue it somehow
-            if (busy) return
-
-            if (!visual && !volume) {
-                throw new Error('volume data missing')
-            } else if (volume && !visual) {
-                busy = true
-                visual = visualCtor(volume)
-                await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
-                busy = false
-            } else {
-                busy = true
-                await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
-                busy = false
-            }
+        return Task.create('Creating or updating VolumeRepresentation', async runtime => {
+            if (!visual) visual = visualCtor()
+            const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
+            if (promise) await promise
             // update list of renderObjects
             renderObjects.length = 0
             if (visual && visual.renderObject) renderObjects.push(visual.renderObject)