diff --git a/src/apps/structure-info/volume.ts b/src/apps/structure-info/volume.ts index 2faaf7086b40f47dd7e77753997805e0d8cbbb81..80564de4b2e06d1b156d6aeb3641a6161376f5ad 100644 --- a/src/apps/structure-info/volume.ts +++ b/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. diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts index a5dce1067c2723641f138d791f7da5d61cb2afec..bb1669a0a5e35896ba1249110a8f4e7b290b612c 100644 --- a/src/mol-model/volume/data.ts +++ b/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 diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts index 581c35cdf37db4e110c2e59933c23233788cc562..07f1e322ec1c4551c56ea6f170f9fff8f847e9c5 100644 --- a/src/mol-repr/volume/direct-volume.ts +++ b/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 }) } diff --git a/src/mol-repr/volume/isosurface-mesh.ts b/src/mol-repr/volume/isosurface-mesh.ts index 272d879992c4c5d12207e5874b04e96e72f547ba..f0d25305018d0e13c9469ff2badaff1bf5a2fb2d 100644 --- a/src/mol-repr/volume/isosurface-mesh.ts +++ b/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' } \ No newline at end of file diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts index ac14576170376528c81837850a63c7b6460812e9..c38081926277f6a485f9df9eb86382c2acdace97 100644 --- a/src/mol-repr/volume/representation.ts +++ b/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)