|
@@ -13,21 +13,21 @@ import { Theme } from '../../mol-theme/theme';
|
|
|
import { createIdentityTransform } from '../../mol-geo/geometry/transform-data';
|
|
|
import { createRenderObject, getNextMaterialId, GraphicsRenderObject } from '../../mol-gl/render-object';
|
|
|
import { PickingId } from '../../mol-geo/geometry/picking';
|
|
|
-import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
|
|
|
-import { Interval } from '../../mol-data/int';
|
|
|
+import { Loci, isEveryLoci, EmptyLoci, isEmptyLoci } from '../../mol-model/loci';
|
|
|
+import { Interval, SortedArray } from '../../mol-data/int';
|
|
|
import { getQualityProps, VisualUpdateState } from '../util';
|
|
|
import { ColorTheme } from '../../mol-theme/color';
|
|
|
import { ValueCell } from '../../mol-util';
|
|
|
import { createSizes } from '../../mol-geo/geometry/size-data';
|
|
|
import { createColors } from '../../mol-geo/geometry/color-data';
|
|
|
import { MarkerAction } from '../../mol-util/marker-action';
|
|
|
-import { Mat4 } from '../../mol-math/linear-algebra';
|
|
|
+import { EPSILON, Mat4 } from '../../mol-math/linear-algebra';
|
|
|
import { Overpaint } from '../../mol-theme/overpaint';
|
|
|
import { Transparency } from '../../mol-theme/transparency';
|
|
|
import { Representation, RepresentationProvider, RepresentationContext, RepresentationParamsGetter } from '../representation';
|
|
|
import { BaseGeometry } from '../../mol-geo/geometry/base';
|
|
|
import { Subject } from 'rxjs';
|
|
|
-import { Task } from '../../mol-task';
|
|
|
+import { RuntimeContext, Task } from '../../mol-task';
|
|
|
import { SizeValues } from '../../mol-gl/renderable/schema';
|
|
|
import { Clipping } from '../../mol-theme/clipping';
|
|
|
import { WebGLContext } from '../../mol-gl/webgl/context';
|
|
@@ -35,7 +35,8 @@ import { isPromiseLike } from '../../mol-util/type-helpers';
|
|
|
import { Substance } from '../../mol-theme/substance';
|
|
|
import { createMarkers } from '../../mol-geo/geometry/marker-data';
|
|
|
|
|
|
-export interface VolumeVisual<P extends VolumeParams> extends Visual<Volume, P> { }
|
|
|
+export type VolumeKey = { volume: Volume, key: number }
|
|
|
+export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeKey, P> { }
|
|
|
|
|
|
function createVolumeRenderObject<G extends Geometry>(volume: Volume, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) {
|
|
|
const { createValues, createRenderableState } = Geometry.getUtils(geometry);
|
|
@@ -47,12 +48,12 @@ function createVolumeRenderObject<G extends Geometry>(volume: Volume, geometry:
|
|
|
|
|
|
interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
|
|
|
defaultProps: PD.Values<P>
|
|
|
- createGeometry(ctx: VisualContext, volume: Volume, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
|
|
|
- createLocationIterator(volume: Volume): LocationIterator
|
|
|
- getLoci(pickingId: PickingId, volume: Volume, props: PD.Values<P>, id: number): Loci
|
|
|
- eachLocation(loci: Loci, volume: Volume, props: PD.Values<P>, apply: (interval: Interval) => boolean): boolean
|
|
|
+ createGeometry(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
|
|
|
+ createLocationIterator(volume: Volume, key: number): LocationIterator
|
|
|
+ getLoci(pickingId: PickingId, volume: Volume, key: number, props: PD.Values<P>, id: number): Loci
|
|
|
+ eachLocation(loci: Loci, volume: Volume, key: number, props: PD.Values<P>, apply: (interval: Interval) => boolean): boolean
|
|
|
setUpdateState(state: VisualUpdateState, volume: Volume, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
|
|
|
- mustRecreate?: (volume: Volume, props: PD.Values<P>) => boolean
|
|
|
+ mustRecreate?: (volumeKey: VolumeKey, props: PD.Values<P>) => boolean
|
|
|
dispose?: (geometry: G) => void
|
|
|
}
|
|
|
|
|
@@ -70,17 +71,19 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
let newProps: PD.Values<P>;
|
|
|
let newTheme: Theme;
|
|
|
let newVolume: Volume;
|
|
|
+ let newKey: number;
|
|
|
|
|
|
let currentProps: PD.Values<P> = Object.assign({}, defaultProps);
|
|
|
let currentTheme: Theme = Theme.createEmpty();
|
|
|
let currentVolume: Volume;
|
|
|
+ let currentKey: number;
|
|
|
|
|
|
let geometry: G;
|
|
|
let geometryVersion = -1;
|
|
|
let locationIt: LocationIterator;
|
|
|
let positionIt: LocationIterator;
|
|
|
|
|
|
- function prepareUpdate(theme: Theme, props: Partial<PD.Values<P>>, volume: Volume) {
|
|
|
+ function prepareUpdate(theme: Theme, props: Partial<PD.Values<P>>, volume: Volume, key: number) {
|
|
|
if (!volume && !currentVolume) {
|
|
|
throw new Error('missing volume');
|
|
|
}
|
|
@@ -88,12 +91,13 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
newProps = Object.assign({}, currentProps, props);
|
|
|
newTheme = theme;
|
|
|
newVolume = volume;
|
|
|
+ newKey = key;
|
|
|
|
|
|
VisualUpdateState.reset(updateState);
|
|
|
|
|
|
if (!renderObject) {
|
|
|
updateState.createNew = true;
|
|
|
- } else if (!currentVolume || !Volume.areEquivalent(newVolume, currentVolume)) {
|
|
|
+ } else if (!Volume.areEquivalent(newVolume, currentVolume) || newKey !== currentKey) {
|
|
|
updateState.createNew = true;
|
|
|
}
|
|
|
|
|
@@ -117,7 +121,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
|
|
|
function update(newGeometry?: G) {
|
|
|
if (updateState.createNew) {
|
|
|
- locationIt = createLocationIterator(newVolume);
|
|
|
+ locationIt = createLocationIterator(newVolume, newKey);
|
|
|
if (newGeometry) {
|
|
|
renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps, materialId);
|
|
|
positionIt = createPositionIterator(newGeometry, renderObject.values);
|
|
@@ -131,7 +135,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
|
|
|
if (updateState.updateTransform) {
|
|
|
// console.log('update transform');
|
|
|
- locationIt = createLocationIterator(newVolume);
|
|
|
+ locationIt = createLocationIterator(newVolume, newKey);
|
|
|
const { instanceCount, groupCount } = locationIt;
|
|
|
if (newProps.instanceGranularity) {
|
|
|
createMarkers(instanceCount, 'instance', renderObject.values);
|
|
@@ -175,18 +179,25 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
currentProps = newProps;
|
|
|
currentTheme = newTheme;
|
|
|
currentVolume = newVolume;
|
|
|
+ currentKey = newKey;
|
|
|
if (newGeometry) {
|
|
|
geometry = newGeometry;
|
|
|
geometryVersion += 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function eachInstance(loci: Loci, volume: Volume, apply: (interval: Interval) => boolean) {
|
|
|
+ function eachInstance(loci: Loci, volume: Volume, key: number, apply: (interval: Interval) => boolean) {
|
|
|
let changed = false;
|
|
|
- if (!Volume.Cell.isLoci(loci)) return false;
|
|
|
- if (Volume.Cell.isLociEmpty(loci)) return false;
|
|
|
- if (!Volume.areEquivalent(loci.volume, volume)) return false;
|
|
|
- if (apply(Interval.ofSingleton(0))) changed = true;
|
|
|
+ if (Volume.Cell.isLoci(loci)) {
|
|
|
+ if (Volume.Cell.isLociEmpty(loci)) return false;
|
|
|
+ if (!Volume.areEquivalent(loci.volume, volume)) return false;
|
|
|
+ if (apply(Interval.ofSingleton(0))) changed = true;
|
|
|
+ } else if (Volume.Segment.isLoci(loci)) {
|
|
|
+ if (Volume.Segment.isLociEmpty(loci)) return false;
|
|
|
+ if (!Volume.areEquivalent(loci.volume, volume)) return false;
|
|
|
+ if (!SortedArray.has(loci.segments, key)) return false;
|
|
|
+ if (apply(Interval.ofSingleton(0))) changed = true;
|
|
|
+ }
|
|
|
return changed;
|
|
|
}
|
|
|
|
|
@@ -199,9 +210,9 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
}
|
|
|
} else {
|
|
|
if (currentProps.instanceGranularity) {
|
|
|
- return eachInstance(loci, currentVolume, apply);
|
|
|
+ return eachInstance(loci, currentVolume, currentKey, apply);
|
|
|
} else {
|
|
|
- return eachLocation(loci, currentVolume, currentProps, apply);
|
|
|
+ return eachLocation(loci, currentVolume, currentKey, currentProps, apply);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -210,17 +221,17 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
|
|
get groupCount() { return locationIt ? locationIt.count : 0; },
|
|
|
get renderObject() { return renderObject; },
|
|
|
get geometryVersion() { return geometryVersion; },
|
|
|
- async createOrUpdate(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}, volume?: Volume) {
|
|
|
- prepareUpdate(theme, props, volume || currentVolume);
|
|
|
+ async createOrUpdate(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}, volumeKey?: VolumeKey) {
|
|
|
+ prepareUpdate(theme, props, volumeKey?.volume || currentVolume, volumeKey?.key || currentKey);
|
|
|
if (updateState.createGeometry) {
|
|
|
- const newGeometry = createGeometry(ctx, newVolume, newTheme, newProps, geometry);
|
|
|
+ const newGeometry = createGeometry(ctx, newVolume, newKey, newTheme, newProps, geometry);
|
|
|
return isPromiseLike(newGeometry) ? newGeometry.then(update) : update(newGeometry);
|
|
|
} else {
|
|
|
update();
|
|
|
}
|
|
|
},
|
|
|
getLoci(pickingId: PickingId) {
|
|
|
- return renderObject ? getLoci(pickingId, currentVolume, currentProps, renderObject.id) : EmptyLoci;
|
|
|
+ return renderObject ? getLoci(pickingId, currentVolume, currentKey, currentProps, renderObject.id) : EmptyLoci;
|
|
|
},
|
|
|
mark(loci: Loci, action: MarkerAction) {
|
|
|
return Visual.mark(renderObject, loci, action, lociApply);
|
|
@@ -278,7 +289,7 @@ export const VolumeParams = {
|
|
|
};
|
|
|
export type VolumeParams = typeof VolumeParams
|
|
|
|
|
|
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, P>, visualCtor: (materialId: number, volume: Volume, props: PD.Values<P>, webgl?: WebGLContext) => VolumeVisual<P>, getLoci: (volume: Volume, props: PD.Values<P>) => Loci): VolumeRepresentation<P> {
|
|
|
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, P>, visualCtor: (materialId: number, volume: Volume, key: number, props: PD.Values<P>, webgl?: WebGLContext) => VolumeVisual<P>, getLoci: (volume: Volume, props: PD.Values<P>) => Loci, getKeys: (props: PD.Values<P>) => ArrayLike<number> = () => [-1]): VolumeRepresentation<P> {
|
|
|
let version = 0;
|
|
|
const { webgl } = ctx;
|
|
|
const updated = new Subject<number>();
|
|
@@ -286,13 +297,27 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
|
|
const materialId = getNextMaterialId();
|
|
|
const renderObjects: GraphicsRenderObject[] = [];
|
|
|
const _state = Representation.createState();
|
|
|
- let visual: VolumeVisual<P> | undefined;
|
|
|
+ const visuals = new Map<number, VolumeVisual<P>>();
|
|
|
|
|
|
let _volume: Volume;
|
|
|
+ let _keys: ArrayLike<number>;
|
|
|
let _params: P;
|
|
|
let _props: PD.Values<P>;
|
|
|
let _theme = Theme.createEmpty();
|
|
|
|
|
|
+ async function visual(runtime: RuntimeContext, key: number) {
|
|
|
+ let visual = visuals.get(key);
|
|
|
+ if (!visual) {
|
|
|
+ visual = visualCtor(materialId, _volume, key, _props, webgl);
|
|
|
+ visuals.set(key, visual);
|
|
|
+ } else if (visual.mustRecreate?.({ volume: _volume, key }, _props, webgl)) {
|
|
|
+ visual.destroy();
|
|
|
+ visual = visualCtor(materialId, _volume, key, _props, webgl);
|
|
|
+ visuals.set(key, visual);
|
|
|
+ }
|
|
|
+ return visual.createOrUpdate({ webgl, runtime }, _theme, _props, { volume: _volume, key });
|
|
|
+ }
|
|
|
+
|
|
|
function createOrUpdate(props: Partial<PD.Values<P>> = {}, volume?: Volume) {
|
|
|
if (volume && volume !== _volume) {
|
|
|
_params = getParams(ctx, volume);
|
|
@@ -301,22 +326,28 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
|
|
}
|
|
|
const qualityProps = getQualityProps(Object.assign({}, _props, props), _volume);
|
|
|
Object.assign(_props, props, qualityProps);
|
|
|
+ _keys = getKeys(_props);
|
|
|
|
|
|
return Task.create('Creating or updating VolumeRepresentation', async runtime => {
|
|
|
- if (!visual) {
|
|
|
- visual = visualCtor(materialId, _volume, _props, webgl);
|
|
|
- } else if (visual.mustRecreate?.(_volume, _props, webgl)) {
|
|
|
- visual.destroy();
|
|
|
- visual = visualCtor(materialId, _volume, _props, webgl);
|
|
|
+ const toDelete = new Set(visuals.keys());
|
|
|
+ for (let i = 0, il = _keys.length; i < il; ++i) {
|
|
|
+ const segment = _keys[i];
|
|
|
+ toDelete.delete(segment);
|
|
|
+ const promise = visual(runtime, segment);
|
|
|
+ if (promise) await promise;
|
|
|
}
|
|
|
- const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, volume);
|
|
|
- if (promise) await promise;
|
|
|
+ toDelete.forEach(segment => {
|
|
|
+ visuals.get(segment)?.destroy();
|
|
|
+ visuals.delete(segment);
|
|
|
+ });
|
|
|
// update list of renderObjects
|
|
|
renderObjects.length = 0;
|
|
|
- if (visual && visual.renderObject) {
|
|
|
- renderObjects.push(visual.renderObject);
|
|
|
- geometryState.add(visual.renderObject.id, visual.geometryVersion);
|
|
|
- }
|
|
|
+ visuals.forEach(visual => {
|
|
|
+ if (visual.renderObject) {
|
|
|
+ renderObjects.push(visual.renderObject);
|
|
|
+ geometryState.add(visual.renderObject.id, visual.geometryVersion);
|
|
|
+ }
|
|
|
+ });
|
|
|
geometryState.snapshot();
|
|
|
// increment version
|
|
|
updated.next(version++);
|
|
@@ -324,16 +355,44 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
|
|
}
|
|
|
|
|
|
function mark(loci: Loci, action: MarkerAction) {
|
|
|
- return visual ? visual.mark(loci, action) : false;
|
|
|
+ let changed = false;
|
|
|
+ visuals.forEach(visual => {
|
|
|
+ changed = visual.mark(loci, action) || changed;
|
|
|
+ });
|
|
|
+ return changed;
|
|
|
}
|
|
|
|
|
|
- function setState(state: Partial<Representation.State>) {
|
|
|
+ function setVisualState(visual: VolumeVisual<P>, state: Partial<Representation.State>) {
|
|
|
if (state.visible !== undefined && visual) visual.setVisibility(state.visible);
|
|
|
if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor);
|
|
|
if (state.pickable !== undefined && visual) visual.setPickable(state.pickable);
|
|
|
if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint);
|
|
|
if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency);
|
|
|
+ if (state.substance !== undefined && visual) visual.setSubstance(state.substance);
|
|
|
+ if (state.clipping !== undefined && visual) visual.setClipping(state.clipping);
|
|
|
if (state.transform !== undefined && visual) visual.setTransform(state.transform);
|
|
|
+ if (state.themeStrength !== undefined && visual) visual.setThemeStrength(state.themeStrength);
|
|
|
+ }
|
|
|
+
|
|
|
+ function setState(state: Partial<Representation.State>) {
|
|
|
+ const { visible, alphaFactor, pickable, overpaint, transparency, substance, clipping, transform, themeStrength, syncManually, markerActions } = state;
|
|
|
+ const newState: Partial<Representation.State> = {};
|
|
|
+
|
|
|
+ if (visible !== _state.visible) newState.visible = visible;
|
|
|
+ if (alphaFactor !== _state.alphaFactor) newState.alphaFactor = alphaFactor;
|
|
|
+ if (pickable !== _state.pickable) newState.pickable = pickable;
|
|
|
+ if (overpaint !== undefined) newState.overpaint = overpaint;
|
|
|
+ if (transparency !== undefined) newState.transparency = transparency;
|
|
|
+ if (substance !== undefined) newState.substance = substance;
|
|
|
+ if (clipping !== undefined) newState.clipping = clipping;
|
|
|
+ if (themeStrength !== undefined) newState.themeStrength = themeStrength;
|
|
|
+ if (transform !== undefined && !Mat4.areEqual(transform, _state.transform, EPSILON)) {
|
|
|
+ newState.transform = transform;
|
|
|
+ }
|
|
|
+ if (syncManually !== _state.syncManually) newState.syncManually = syncManually;
|
|
|
+ if (markerActions !== _state.markerActions) newState.markerActions = markerActions;
|
|
|
+
|
|
|
+ visuals.forEach(visual => setVisualState(visual, newState));
|
|
|
|
|
|
Representation.updateState(_state, state);
|
|
|
}
|
|
@@ -343,13 +402,18 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
|
|
}
|
|
|
|
|
|
function destroy() {
|
|
|
- if (visual) visual.destroy();
|
|
|
+ visuals.forEach(visual => visual.destroy());
|
|
|
+ visuals.clear();
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
label,
|
|
|
get groupCount() {
|
|
|
- return visual ? visual.groupCount : 0;
|
|
|
+ let groupCount = 0;
|
|
|
+ visuals.forEach(visual => {
|
|
|
+ if (visual.renderObject) groupCount += visual.groupCount;
|
|
|
+ });
|
|
|
+ return groupCount;
|
|
|
},
|
|
|
get props() { return _props; },
|
|
|
get params() { return _params; },
|
|
@@ -362,7 +426,12 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
|
|
setState,
|
|
|
setTheme,
|
|
|
getLoci: (pickingId: PickingId): Loci => {
|
|
|
- return visual ? visual.getLoci(pickingId) : EmptyLoci;
|
|
|
+ let loci: Loci = EmptyLoci;
|
|
|
+ visuals.forEach(visual => {
|
|
|
+ const _loci = visual.getLoci(pickingId);
|
|
|
+ if (!isEmptyLoci(_loci)) loci = _loci;
|
|
|
+ });
|
|
|
+ return loci;
|
|
|
},
|
|
|
getAllLoci: (): Loci[] => {
|
|
|
return [getLoci(_volume, _props)];
|