diff --git a/python/examples/example_annotation_properties.py b/python/examples/example_annotation_properties.py index e957038105..1d4dcca9bd 100644 --- a/python/examples/example_annotation_properties.py +++ b/python/examples/example_annotation_properties.py @@ -27,18 +27,28 @@ id='size', type='float32', default=10, - ) + ), + neuroglancer.AnnotationPropertySpec( + id='p_int8', + type='int8', + default=10, + ), + neuroglancer.AnnotationPropertySpec( + id='p_uint8', + type='uint8', + default=10, + ), ], annotations=[ neuroglancer.PointAnnotation( id='1', point=[150, 150], - props=['#0f0', 5], + props=['#0f0', 5, 6, 7], ), neuroglancer.PointAnnotation( id='2', point=[250, 100], - props=['#ff0', 30], + props=['#ff0', 30, 7, 9], ), ], shader=''' diff --git a/src/neuroglancer/annotation/annotation_layer_state.ts b/src/neuroglancer/annotation/annotation_layer_state.ts index 33047acf98..fe6048baf4 100644 --- a/src/neuroglancer/annotation/annotation_layer_state.ts +++ b/src/neuroglancer/annotation/annotation_layer_state.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {AnnotationSource} from 'neuroglancer/annotation'; +import {AnnotationPropertySpec, AnnotationSource, propertyTypeDataType} from 'neuroglancer/annotation'; import {MultiscaleAnnotationSource} from 'neuroglancer/annotation/frontend_source'; import {LayerDataSource} from 'neuroglancer/layer_data_source'; import {ChunkTransformParameters, getChunkTransformParameters, RenderLayerTransformOrError} from 'neuroglancer/render_coordinate_transform'; @@ -29,6 +29,7 @@ import {vec3} from 'neuroglancer/util/geom'; import {WatchableMap} from 'neuroglancer/util/watchable_map'; import {makeTrackableFragmentMain, makeWatchableShaderError} from 'neuroglancer/webgl/dynamic_shader'; import {getFallbackBuilderState, parseShaderUiControls, ShaderControlState} from 'neuroglancer/webgl/shader_ui_controls'; +import { DataType } from '../util/data_type'; export class AnnotationHoverState extends WatchableValue< {id: string, partIndex: number, annotationLayerState: AnnotationLayerState}|undefined> {} @@ -87,8 +88,21 @@ void main() { `; export class AnnotationDisplayState extends RefCounted { + annotationProperties = new WatchableValue(undefined); shader = makeTrackableFragmentMain(DEFAULT_FRAGMENT_MAIN); - shaderControls = new ShaderControlState(this.shader); + shaderControls = new ShaderControlState( + this.shader, makeCachedLazyDerivedWatchableValue(annotationProperties => { + const properties = new Map(); + if (annotationProperties === undefined) { + return null; + } + for (const property of annotationProperties) { + const dataType = propertyTypeDataType[property.type]; + if (dataType === undefined) continue; + properties.set(property.identifier, dataType); + } + return {properties}; + }, this.annotationProperties)); fallbackShaderControls = new WatchableValue(getFallbackBuilderState(parseShaderUiControls(DEFAULT_FRAGMENT_MAIN))); shaderError = makeWatchableShaderError(); diff --git a/src/neuroglancer/annotation/index.ts b/src/neuroglancer/annotation/index.ts index bac8651d1d..36c41cc6b1 100644 --- a/src/neuroglancer/annotation/index.ts +++ b/src/neuroglancer/annotation/index.ts @@ -80,6 +80,18 @@ export interface AnnotationNumericPropertySpec extends AnnotationPropertySpecBas step?: number; } +export const propertyTypeDataType: Record = { + 'float32': DataType.FLOAT32, + 'uint32': DataType.UINT32, + 'int32': DataType.INT32, + 'uint16': DataType.UINT16, + 'int16': DataType.INT16, + 'uint8': DataType.UINT8, + 'int8': DataType.INT8, + 'rgb': undefined, + 'rgba': undefined, +}; + export type AnnotationPropertySpec = AnnotationColorPropertySpec|AnnotationNumericPropertySpec; export interface AnnotationPropertyTypeHandler { diff --git a/src/neuroglancer/annotation/renderlayer.ts b/src/neuroglancer/annotation/renderlayer.ts index 5868027c60..9acd738875 100644 --- a/src/neuroglancer/annotation/renderlayer.ts +++ b/src/neuroglancer/annotation/renderlayer.ts @@ -353,6 +353,8 @@ function AnnotationRenderLayer) { @@ -453,6 +455,8 @@ function AnnotationRenderLayer 0; for (const annotationType of annotationTypes) { const idMap = typeToIdMaps[annotationType]; let count = idMap.size; @@ -471,7 +475,12 @@ function AnnotationRenderLayer; @@ -124,21 +127,17 @@ const annotationPropertyTypeRenderHandlers: 'int8': makeIntegerPropertyRenderHandler('highp int', 1, WebGL2RenderingContext.BYTE), }; -export abstract class AnnotationRenderHelper extends RefCounted { - pickIdsPerInstance: number; - targetIsSliceView: boolean; +class AnnotationRenderHelperBase extends RefCounted { readonly serializedBytesPerAnnotation: number; readonly serializedGeometryBytesPerAnnotation: number; readonly propertyOffsets: {group: number, offset: number}[]; readonly propertyGroupBytes: number[]; + readonly propertyGroupCumulativeBytes: number[]; readonly geometryDataStride: number; constructor( public gl: GL, public annotationType: AnnotationType, public rank: number, - public properties: readonly Readonly[], - public shaderControlState: ShaderControlState, - public fallbackShaderParameters: WatchableValueInterface, - public shaderError: WatchableShaderError) { + public properties: readonly Readonly[]) { super(); const serializedGeometryBytesPerAnnotation = this.serializedGeometryBytesPerAnnotation = annotationTypeHandlers[annotationType].serializedBytes(rank); @@ -148,6 +147,63 @@ export abstract class AnnotationRenderHelper extends RefCounted { this.propertyOffsets = offsets; this.propertyGroupBytes = propertyGroupBytes; this.geometryDataStride = propertyGroupBytes[0]; + const propertyGroupCumulativeBytes = this.propertyGroupCumulativeBytes = + new Array(propertyGroupBytes.length); + propertyGroupCumulativeBytes[0] = 0; + for (let i = 1; i < propertyGroupBytes.length; ++i) { + propertyGroupCumulativeBytes[i] = + propertyGroupCumulativeBytes[i - 1] + propertyGroupBytes[i - 1]; + } + } + + protected defineProperties(builder: ShaderBuilder, referencedProperties: number[]) { + const {properties, rank} = this; + for (const i of referencedProperties) { + const property = properties[i]; + const handler = annotationPropertyTypeRenderHandlers[property.type]; + handler.defineShader(builder, property.identifier, rank); + } + const {propertyOffsets} = this; + const {propertyGroupBytes, propertyGroupCumulativeBytes} = this; + builder.addInitializer(shader => { + const binders = referencedProperties.map( + i => shader.vertexShaderInputBinders[`prop_${properties[i].identifier}`]); + const numProperties = binders.length; + shader.vertexShaderInputBinders['properties'] = { + enable(divisor: number) { + for (let i = 0; i < numProperties; ++i) { + binders[i].enable(divisor); + } + }, + bind(stride: number, offset: number) { + for (let i = 0; i < numProperties; ++i) { + const {group, offset: propertyOffset} = propertyOffsets[referencedProperties[i]]; + binders[i].bind( + /*stride=*/ propertyGroupBytes[group], + /*offset=*/ offset + propertyOffset + propertyGroupCumulativeBytes[group] * stride); + } + }, + disable() { + for (let i = 0; i < numProperties; ++i) { + binders[i].disable(); + } + }, + }; + }); + } +} + +export abstract class AnnotationRenderHelper extends AnnotationRenderHelperBase { + pickIdsPerInstance: number; + targetIsSliceView: boolean; + + constructor( + gl: GL, annotationType: AnnotationType, rank: number, + properties: readonly Readonly[], + public shaderControlState: ShaderControlState, + public fallbackShaderParameters: WatchableValueInterface, + public shaderError: WatchableShaderError) { + super(gl, annotationType, rank, properties); } getDependentShader(memoizeKey: any, defineShader: (builder: ShaderBuilder) => void): @@ -167,49 +223,18 @@ export abstract class AnnotationRenderHelper extends RefCounted { defineShader: (builder: ShaderBuilder, parameters: ShaderControlsBuilderState) => { const {rank, properties} = this; const referencedProperties: number[] = []; + const controlsReferencedProperties = parameters.referencedProperties; const processedCode = parameters.parseResult.code; for (let i = 0, numProperties = properties.length; i < numProperties; ++i) { const property = properties[i]; const functionName = `prop_${property.identifier}`; - if (!processedCode.match(new RegExp(`\\b${functionName}\\b`))) continue; + if (!controlsReferencedProperties.includes(property.identifier) && + !processedCode.match(new RegExp(`\\b${functionName}\\b`))) { + continue; + } referencedProperties.push(i); - const handler = annotationPropertyTypeRenderHandlers[property.type]; - handler.defineShader(builder, property.identifier, rank); - } - const {propertyOffsets} = this; - const {propertyGroupBytes} = this; - const propertyGroupCumulativeBytes = new Array(propertyGroupBytes.length); - propertyGroupCumulativeBytes[0] = 0; - for (let i = 1; i < propertyGroupBytes.length; ++i) { - propertyGroupCumulativeBytes[i] = - propertyGroupCumulativeBytes[i - 1] + propertyGroupBytes[i - 1]; } - builder.addInitializer(shader => { - const binders = referencedProperties.map( - i => shader.vertexShaderInputBinders[`prop_${properties[i].identifier}`]); - const numProperties = binders.length; - shader.vertexShaderInputBinders['properties'] = { - enable(divisor: number) { - for (let i = 0; i < numProperties; ++i) { - binders[i].enable(divisor); - } - }, - bind(stride: number, offset: number) { - for (let i = 0; i < numProperties; ++i) { - let {group, offset: propertyOffset} = propertyOffsets[referencedProperties[i]]; - binders[i].bind( - /*stride=*/ propertyGroupBytes[group], - /*offset=*/ offset + propertyOffset + - propertyGroupCumulativeBytes[group] * stride); - } - }, - disable() { - for (let i = 0; i < numProperties; ++i) { - binders[i].disable(); - } - }, - }; - }); + this.defineProperties(builder, referencedProperties); builder.addUniform('highp vec3', 'uColor'); builder.addUniform('highp uint', 'uSelectedIndex'); builder.addVarying('highp vec4', 'vColor'); @@ -433,6 +458,103 @@ if (ng_discardValue) { } abstract draw(context: AnnotationRenderContext): void; + + private histogramShaders = new Map(); + + private getHistogramShader(propertyType: AnnotationPropertySpec['type']): ShaderProgram { + const {histogramShaders} = this; + let shader = histogramShaders.get(propertyType); + if (shader === undefined) { + const {gl} = this; + shader = gl.memoize.get( + JSON.stringify({t: 'propertyHistogramGenerator', propertyType}), () => { + const builder = new ShaderBuilder(gl); + this.defineHistogramShader(builder, propertyType); + return builder.build(); + }); + histogramShaders.set(propertyType, shader); + } + return shader; + } + + private defineHistogramShader(builder: ShaderBuilder, propertyType: AnnotationPropertySpec['type']) { + const handler = annotationPropertyTypeRenderHandlers[propertyType]; + // TODO(jbms): If rank-dependent properties are added, this will need to change to support + // histograms. + handler.defineShader(builder, 'histogram', /*rank=*/ 0); + builder.addOutputBuffer('vec4', 'out_histogram', 0); + const invlerpName = `invlerpForHistogram`; + const dataType = propertyTypeDataType[propertyType]!; + builder.addVertexCode( + defineInvlerpShaderFunction(builder, invlerpName, dataType, /*clamp=*/ false)); + builder.setVertexMain(` +float x = invlerpForHistogram(prop_histogram()); +if (x < 0.0) x = 0.0; +else if (x > 1.0) x = 1.0; +else x = (1.0 + x * 253.0) / 255.0; +gl_Position = vec4(2.0 * (x * 255.0 + 0.5) / 256.0 - 1.0, 0.0, 0.0, 1.0); +gl_PointSize = 1.0; +`); + builder.setFragmentMain(`out_histogram = vec4(1.0, 1.0, 1.0, 1.0);`); + } + + computeHistograms(context: AnnotationRenderContext, frameNumber: number) { + const {histogramSpecifications} = this.shaderControlState; + const histogramProperties = histogramSpecifications.properties.value; + const numHistograms = histogramProperties.length; + const {properties} = this; + const numProperties = properties.length; + const {propertyOffsets} = this; + const {propertyGroupBytes, propertyGroupCumulativeBytes} = this; + const {gl} = this; + gl.enable(WebGL2RenderingContext.BLEND); + gl.disable(WebGL2RenderingContext.SCISSOR_TEST); + gl.disable(WebGL2RenderingContext.DEPTH_TEST); + gl.blendFunc(WebGL2RenderingContext.ONE, WebGL2RenderingContext.ONE); + const outputFramebuffers = histogramSpecifications.getFramebuffers(gl); + const oldFrameNumber = histogramSpecifications.frameNumber; + histogramSpecifications.frameNumber = frameNumber; + gl.bindBuffer(WebGL2RenderingContext.ARRAY_BUFFER, context.buffer.buffer); + for (let histogramIndex = 0; histogramIndex < numHistograms; ++histogramIndex) { + const propertyIdentifier = histogramProperties[histogramIndex]; + for (let propertyIndex = 0; propertyIndex < numProperties; ++propertyIndex) { + const property = properties[propertyIndex]; + if (property.identifier !== propertyIdentifier) continue; + const propertyType = property.type; + const dataType = propertyTypeDataType[propertyType]!; + const shader = this.getHistogramShader(propertyType); + shader.bind(); + const binder = shader.vertexShaderInputBinders['prop_histogram']; + binder.enable(0); + const {group, offset: propertyOffset} = propertyOffsets[propertyIndex]; + enableLerpShaderFunction( + shader, `invlerpForHistogram`, dataType, histogramSpecifications.bounds.value[histogramIndex]); + binder.bind( + /*stride=*/ propertyGroupBytes[group], + /*offset=*/ context.bufferOffset + propertyOffset + + propertyGroupCumulativeBytes[group] * context.count); + outputFramebuffers[histogramIndex].bind(256, 1); + if (frameNumber !== oldFrameNumber) { + gl.clearColor(0, 0, 0, 0); + gl.clear(WebGL2RenderingContext.COLOR_BUFFER_BIT); + } + gl.drawArrays(WebGL2RenderingContext.POINTS, 0, context.count); + if (DEBUG_HISTOGRAMS) { + const tempBuffer = new Float32Array(256 * 4); + gl.readPixels( + 0, 0, 256, 1, WebGL2RenderingContext.RGBA, WebGL2RenderingContext.FLOAT, tempBuffer); + const tempBuffer2 = new Float32Array(256); + for (let j = 0; j < 256; ++j) { + tempBuffer2[j] = tempBuffer[j * 4]; + } + console.log('histogram', tempBuffer2.join(' ')); + } + binder.disable(); + break; + } + } + gl.disable(WebGL2RenderingContext.BLEND); + } } interface AnnotationRenderHelperConstructor { diff --git a/src/neuroglancer/annotation/user_layer.ts b/src/neuroglancer/annotation/user_layer.ts index 17abec2eca..be928f26e5 100644 --- a/src/neuroglancer/annotation/user_layer.ts +++ b/src/neuroglancer/annotation/user_layer.ts @@ -29,7 +29,7 @@ import {RenderLayerRole} from 'neuroglancer/renderlayer'; import {SegmentationDisplayState} from 'neuroglancer/segmentation_display_state/frontend'; import {SegmentationUserLayer} from 'neuroglancer/segmentation_user_layer'; import {TrackableBoolean, TrackableBooleanCheckbox} from 'neuroglancer/trackable_boolean'; -import {makeCachedLazyDerivedWatchableValue, WatchableValue} from 'neuroglancer/trackable_value'; +import {makeCachedLazyDerivedWatchableValue} from 'neuroglancer/trackable_value'; import {AnnotationLayerView, MergedAnnotationStates, UserLayerWithAnnotationsMixin} from 'neuroglancer/ui/annotations'; import {animationFrameDebounce} from 'neuroglancer/util/animation_frame_debounce'; import {Borrowed, Owned, RefCounted} from 'neuroglancer/util/disposable'; @@ -317,7 +317,6 @@ export class AnnotationUserLayer extends Base { localAnnotations: LocalAnnotationSource|undefined; private localAnnotationProperties: AnnotationPropertySpec[]|undefined; private localAnnotationRelationships: string[]; - annotationProperties = new WatchableValue(undefined); private localAnnotationsJson: any = undefined; private pointAnnotationsJson: any = undefined; linkedSegmentationLayers = this.registerDisposer(new LinkedSegmentationLayers( @@ -477,9 +476,9 @@ export class AnnotationUserLayer extends Base { } loadedSubsource.deactivate('Not compatible with annotation layer'); } - const prevAnnotationProperties = this.annotationProperties.value; + const prevAnnotationProperties = this.annotationDisplayState.annotationProperties.value; if (stableStringify(prevAnnotationProperties) !== stableStringify(properties)) { - this.annotationProperties.value = properties; + this.annotationDisplayState.annotationProperties.value = properties; } } @@ -575,7 +574,7 @@ class RenderingOptionsTab extends Tab { element.appendChild( this .registerDisposer(new DependentViewWidget( - layer.annotationProperties, + layer.annotationDisplayState.annotationProperties, (properties, parent) => { if (properties === undefined || properties.length === 0) return; const propertyList = document.createElement('div'); diff --git a/src/neuroglancer/perspective_view/panel.ts b/src/neuroglancer/perspective_view/panel.ts index 6aa01bf702..573d6bf400 100644 --- a/src/neuroglancer/perspective_view/panel.ts +++ b/src/neuroglancer/perspective_view/panel.ts @@ -458,7 +458,10 @@ export class PerspectivePanel extends RenderedDataPanel { } let gl = this.gl; - this.offscreenFramebuffer.bind(width, height); + const bindFramebuffer = () => { + this.offscreenFramebuffer.bind(width, height); + }; + bindFramebuffer(); gl.disable(gl.SCISSOR_TEST); // Stencil buffer bit 0 indicates positions of framebuffer written by an opaque layer. @@ -533,6 +536,8 @@ export class PerspectivePanel extends RenderedDataPanel { emitColor: true, emitPickID: true, alreadyEmittedPickID: false, + bindFramebuffer, + frameNumber: this.context.frameNumber, }; mat4.copy(pickingData.invTransform, projectionParameters.invViewProjectionMat); @@ -559,6 +564,7 @@ export class PerspectivePanel extends RenderedDataPanel { if (hasAnnotation) { // Render annotations with blending enabled. + gl.enable(WebGL2RenderingContext.BLEND); gl.depthFunc(WebGL2RenderingContext.LEQUAL); gl.blendFunc(WebGL2RenderingContext.SRC_ALPHA, WebGL2RenderingContext.ONE_MINUS_SRC_ALPHA); @@ -587,7 +593,10 @@ export class PerspectivePanel extends RenderedDataPanel { // Compute accumulate and revealage textures. const {transparentConfiguration} = this; - transparentConfiguration.bind(width, height); + renderContext.bindFramebuffer = () => { + transparentConfiguration.bind(width, height); + }; + renderContext.bindFramebuffer(); this.gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(WebGL2RenderingContext.COLOR_BUFFER_BIT); renderContext.emitter = perspectivePanelEmitOIT; @@ -613,7 +622,8 @@ export class PerspectivePanel extends RenderedDataPanel { gl.enable(WebGL2RenderingContext.DEPTH_TEST); // Restore framebuffer attachments. - this.offscreenFramebuffer.bind(width, height); + renderContext.bindFramebuffer = bindFramebuffer; + bindFramebuffer(); // Do picking only rendering pass for transparent layers. gl.enable(WebGL2RenderingContext.STENCIL_TEST); diff --git a/src/neuroglancer/renderlayer.ts b/src/neuroglancer/renderlayer.ts index b384c46b7a..74f3fc6c14 100644 --- a/src/neuroglancer/renderlayer.ts +++ b/src/neuroglancer/renderlayer.ts @@ -95,6 +95,8 @@ export interface ThreeDimensionalReadyRenderContext { export interface ThreeDimensionalRenderContext extends ThreeDimensionalReadyRenderContext { pickIDs: PickIDManager; wireFrame: boolean; + bindFramebuffer: () => void; + frameNumber: number; } diff --git a/src/neuroglancer/sliceview/image_layer_rendering.md b/src/neuroglancer/sliceview/image_layer_rendering.md index b609de3db8..08bb84c531 100644 --- a/src/neuroglancer/sliceview/image_layer_rendering.md +++ b/src/neuroglancer/sliceview/image_layer_rendering.md @@ -89,16 +89,27 @@ either `false` or `true` according to the state of the checkbox. ### `invlerp` controls -The `invlerp` control type allows the user to specify an interval of the layer's data type that is -linearly mapped to a `float` in the interval `[0, 1]`. The name `invlerp` refers to *inverse linear -interpolation*. To aid the selection of the interval, an empirical cumulative distribution function -(ECDF) of the currently displayed data is plotted as part of the control. Additionally, if there -are no channel dimensions, a color legend is also displayed. +The `invlerp` control type allows the user to specify an numerical interval that is linearly mapped +to a `float` in the interval `[0, 1]`. The name `invlerp` refers to *inverse linear interpolation*. +To aid the selection of the interval, an empirical cumulative distribution function (ECDF) of the +currently displayed data is plotted as part of the control. + + +This control is supported both for image layers and annotation layers. + +- For image layers, if there are channel dimensions, the ECDF is always computed over just a single + channel; the default channel is specified by the `channel` parameter and may be adjusted using the + UI. If there are no channel dimensions, a color legend is also displayed that shows the output + color for each input value in the ECDF domain. + +- For annotation layers, the ECDF is computed over a single numerical property; the default property + is specified by the `property` parameter and may be adjusted using the UI. Directive syntax: ```glsl #uicontrol invlerp (range=[3, 75], window=[0, 100], channel=[1,2], clamp=false) +#uicontrol invlerp (range=[3, 75], window=[0, 100], property="e1", clamp=false) ``` The following parameters are supported: @@ -111,10 +122,14 @@ The following parameters are supported: - `window`: Optional. The default interval over which the ECDF will be shown. May be overridden using the UI control. If not specified, defaults to the interval specified for `range`. -- `channel`: Optional. The channel for which to compute the ECDF. If the rank of the channel - coordinate space is 1, may be specified as a single number, e.g. `channel=2`. Otherwise, must be - specified as an array, e.g. `channel=[2, 3]`. May be overriden using the UI control. If not - specified, defaults to all-zero channel coordinates. +- `channel`: Optional. The channel for which to compute the ECDF (supported for image layers only). + If the rank of the channel coordinate space is 1, may be specified as a single number, + e.g. `channel=2`. Otherwise, must be specified as an array, e.g. `channel=[2, 3]`. May be + overriden using the UI control. If not specified, defaults to all-zero channel coordinates. + +- `property`: Optional. The property for which to compute the ECDF (supported for annotation layers + only). Must be specified as a string, e.g. `property="e1"`. May be overriden using the UI + control. If not specified, defaults to the first numerical property. - `clamp`: Optional. Indicates whether to clamp the result to `[0, 1]`. Defaults to `true`. If `false`, the result will be outside `[0, 1]` if the input value is outside the configured range. @@ -124,15 +139,22 @@ This directive makes the following shader functions available: ```glsl float (T value); + +// For image layers float () { return (getDataValue(channel...)); } + +// For annotation layers +float () { + return (prop_()); +} ``` -where `T` is the data type returned by `getDataValue`. The one-parameter overload simply computes -the inverse linear interpolation of the specified value within the range specified by the control. -The zero-parameter overload returns the inverse linear interpolation of the data value for -configured channel. +where `T` is the data type returned by `getDataValue` (for image layers) or `prop_` (for +annotation layers). The one-parameter overload simply computes the inverse linear interpolation of +the specified value within the range specified by the control. The zero-parameter overload returns +the inverse linear interpolation of the data value for configured channel/property. ## API diff --git a/src/neuroglancer/sliceview/panel.ts b/src/neuroglancer/sliceview/panel.ts index 78d2565e25..fb7a65c628 100644 --- a/src/neuroglancer/sliceview/panel.ts +++ b/src/neuroglancer/sliceview/panel.ts @@ -255,6 +255,15 @@ export class SliceViewPanel extends RenderedDataPanel { let {pickIDs} = this; pickIDs.clear(); + const bindFramebuffer = () => { + gl.disable(WebGL2RenderingContext.SCISSOR_TEST); + gl.enable(WebGL2RenderingContext.BLEND); + gl.blendFunc(WebGL2RenderingContext.SRC_ALPHA, WebGL2RenderingContext.ONE_MINUS_SRC_ALPHA); + this.offscreenFramebuffer.bind(width, height); + }; + + bindFramebuffer(); + const renderContext: SliceViewPanelRenderContext = { wireFrame: this.viewer.wireFrame.value, projectionParameters, @@ -263,10 +272,9 @@ export class SliceViewPanel extends RenderedDataPanel { emitColor: true, emitPickID: true, sliceView, + bindFramebuffer, + frameNumber: this.context.frameNumber, }; - this.offscreenFramebuffer.bind(width, height); - gl.enable(WebGL2RenderingContext.BLEND); - gl.blendFunc(WebGL2RenderingContext.SRC_ALPHA, WebGL2RenderingContext.ONE_MINUS_SRC_ALPHA); for (const [renderLayer, attachment] of visibleLayers) { renderLayer.draw(renderContext, attachment); } diff --git a/src/neuroglancer/sliceview/renderlayer.ts b/src/neuroglancer/sliceview/renderlayer.ts index 842ea264e6..fe5e0e1627 100644 --- a/src/neuroglancer/sliceview/renderlayer.ts +++ b/src/neuroglancer/sliceview/renderlayer.ts @@ -80,9 +80,7 @@ export abstract class SliceViewRenderLayer< dataHistogramSpecifications: HistogramSpecifications; getDataHistogramCount() { - const {dataHistogramSpecifications} = this; - if (!dataHistogramSpecifications.visibility.visible) return 0; - return dataHistogramSpecifications.bounds.value.length; + return this.dataHistogramSpecifications.visibleHistograms; } /** @@ -154,7 +152,8 @@ export abstract class SliceViewRenderLayer< this.rpcTransfer = options.rpcTransfer || {}; this.dataHistogramSpecifications = this.registerDisposer( options.dataHistogramSpecifications ?? - new HistogramSpecifications(constantWatchableValue([]), constantWatchableValue([]))); + new HistogramSpecifications( + constantWatchableValue([]), constantWatchableValue([]), constantWatchableValue([]))); this.registerDisposer( this.dataHistogramSpecifications.visibility.changed.add(this.redrawNeeded.dispatch)); } diff --git a/src/neuroglancer/util/lerp.ts b/src/neuroglancer/util/lerp.ts index d87ea13f82..62edbaff91 100644 --- a/src/neuroglancer/util/lerp.ts +++ b/src/neuroglancer/util/lerp.ts @@ -21,6 +21,8 @@ import {Uint64} from 'neuroglancer/util/uint64'; export type DataTypeInterval = [number, number]|[Uint64, Uint64]; +export type UnknownDataTypeInterval = [number|Uint64, number|Uint64]; + export const defaultDataTypeRange: Record = { [DataType.UINT8]: [0, 0xff], [DataType.INT8]: [-0x80, 0x7f], @@ -172,11 +174,35 @@ export function parseDataTypeValue(dataType: DataType, x: unknown): number|Uint6 } } +export function parseUnknownDataTypeValue(x: unknown): number|Uint64 { + if (typeof x === 'number') return x; + if (typeof x === 'string') { + let num64 = new Uint64(); + let num = Number(x); + if (num64.tryParseString(x)) { + if (num.toString() === num64.toString()) { + return num; + } + return num64; + } + if (!Number.isFinite(num)) { + throw new Error(`Invalid value: ${JSON.stringify(x)}`); + } + return num; + } + throw new Error(`Invalid value: ${JSON.stringify(x)}`); +} + export function parseDataTypeInterval(obj: unknown, dataType: DataType): DataTypeInterval { return parseFixedLengthArray(new Array(2), obj, x => parseDataTypeValue(dataType, x)) as DataTypeInterval; } +export function parseUnknownDataTypeInterval(obj: unknown): UnknownDataTypeInterval { + return parseFixedLengthArray(new Array(2), obj, x => parseUnknownDataTypeValue(x)) as + UnknownDataTypeInterval; +} + export function dataTypeIntervalEqual( dataType: DataType, a: DataTypeInterval, b: DataTypeInterval) { if (dataType === DataType.UINT64) { @@ -252,3 +278,43 @@ export function getIntervalBoundsEffectiveFraction(dataType: DataType, interval: } } } + +export function convertDataTypeInterval( + interval: UnknownDataTypeInterval|undefined, dataType: DataType): DataTypeInterval { + if (interval === undefined) { + return defaultDataTypeRange[dataType]; + } + let [lower, upper] = interval; + if (dataType === DataType.UINT64) { + if (typeof lower === 'number') { + lower = Uint64.fromNumber(lower); + } + if (typeof upper === 'number') { + upper = Uint64.fromNumber(upper); + } + return [lower, upper]; + } + // Ensure that neither lower nor upper is a `Uint64`. + if (typeof lower !== 'number') { + lower = lower.toNumber(); + } + if (typeof upper !== 'number') { + upper = upper.toNumber(); + } + if (dataType !== DataType.FLOAT32) { + lower = Math.round(lower); + upper = Math.round(upper); + const range = defaultDataTypeRange[dataType] as [number, number]; + if (!Number.isFinite(lower)) { + lower = range[0]; + } else { + lower = Math.min(Math.max(range[0], lower), range[1]); + } + if (!Number.isFinite(upper)) { + upper = range[1]; + } else { + upper = Math.min(Math.max(range[0], upper), range[1]); + } + } + return [lower, upper]; +} diff --git a/src/neuroglancer/util/uint64.ts b/src/neuroglancer/util/uint64.ts index d8cb3a36da..37908c5efd 100644 --- a/src/neuroglancer/util/uint64.ts +++ b/src/neuroglancer/util/uint64.ts @@ -333,4 +333,10 @@ export class Uint64 { this.high = Math.floor(value / 0x100000000); } } + + static fromNumber(value: number) { + const x = new Uint64(); + x.setFromNumber(value); + return x; + } } diff --git a/src/neuroglancer/webgl/empirical_cdf.ts b/src/neuroglancer/webgl/empirical_cdf.ts index e054ec317d..49d15385e3 100644 --- a/src/neuroglancer/webgl/empirical_cdf.ts +++ b/src/neuroglancer/webgl/empirical_cdf.ts @@ -48,6 +48,7 @@ export class HistogramSpecifications extends RefCounted { frameNumber = -1; constructor( public channels: WatchableValueInterface, + public properties: WatchableValueInterface, public bounds: WatchableValueInterface, public visibility = new VisibilityPriorityAggregator()) { super(); @@ -55,7 +56,8 @@ export class HistogramSpecifications extends RefCounted { getFramebuffers(gl: GL) { const {framebuffers} = this; - while (framebuffers.length < this.channels.value.length) { + const count = this.bounds.value.length; + while (framebuffers.length < count) { const framebuffer = new FramebufferConfiguration(gl, { colorBuffers: makeTextureBuffers( gl, 1, WebGL2RenderingContext.R32F, WebGL2RenderingContext.RED, @@ -66,6 +68,11 @@ export class HistogramSpecifications extends RefCounted { return framebuffers; } + get visibleHistograms(): number { + if (!this.visibility.visible) return 0; + return this.bounds.value.length; + } + disposed() { for (const framebuffer of this.framebuffers) { framebuffer.dispose(); diff --git a/src/neuroglancer/webgl/lerp.ts b/src/neuroglancer/webgl/lerp.ts index 70cd29b7bb..c208d274af 100644 --- a/src/neuroglancer/webgl/lerp.ts +++ b/src/neuroglancer/webgl/lerp.ts @@ -18,7 +18,7 @@ * @file Defines lerp/invlerp functionality for all supported data types. */ -import {DataType} from 'neuroglancer/util/data_type'; +import {DataType, DATA_TYPE_SIGNED} from 'neuroglancer/util/data_type'; import {DataTypeInterval} from 'neuroglancer/util/lerp'; import {Uint64} from 'neuroglancer/util/uint64'; import {ShaderBuilder, ShaderCodePart, ShaderProgram} from 'neuroglancer/webgl/shader'; @@ -66,17 +66,23 @@ struct Uint64LerpParameters { ], }; +const glsl_computeInvlerpFloat = ` +float computeInvlerp(float inputValue, vec2 p) { + float outputValue = inputValue; + outputValue = (outputValue - p[0]) * p[1]; + return outputValue; +} +`; -function getFloatInvlerpImpl(dataType: DataType) { + +function getIntFloatInvlerpImpl(dataType: DataType) { const shaderDataType = getShaderType(dataType); let code = ` float computeInvlerp(${shaderDataType} inputValue, vec2 p) { - float outputValue = float(toRaw(inputValue)); - outputValue = (outputValue - p[0]) * p[1]; - return outputValue; + return computeInvlerp(float(toRaw(inputValue)), p); } `; - return [dataTypeShaderDefinition[dataType], code]; + return [dataTypeShaderDefinition[dataType], glsl_computeInvlerpFloat, code]; } function getInt32InvlerpImpl(dataType: DataType) { @@ -99,16 +105,19 @@ float computeInvlerp(${shaderDataType} inputValue, ${pType} p) { x >>= p.shift; return float(x) * p.multiplier; } +float computeInvlerp(${shaderDataType} inputValue, ${pType} p) { + return computeInvlerp(toRaw(inputValue), p); +} `, ]; } export const glsl_dataTypeComputeInvlerp: Record = { - [DataType.UINT8]: getFloatInvlerpImpl(DataType.UINT8), - [DataType.INT8]: getFloatInvlerpImpl(DataType.INT8), - [DataType.UINT16]: getFloatInvlerpImpl(DataType.UINT16), - [DataType.INT16]: getFloatInvlerpImpl(DataType.INT16), - [DataType.FLOAT32]: getFloatInvlerpImpl(DataType.FLOAT32), + [DataType.UINT8]: getIntFloatInvlerpImpl(DataType.UINT8), + [DataType.INT8]: getIntFloatInvlerpImpl(DataType.INT8), + [DataType.UINT16]: getIntFloatInvlerpImpl(DataType.UINT16), + [DataType.INT16]: getIntFloatInvlerpImpl(DataType.INT16), + [DataType.FLOAT32]: glsl_computeInvlerpFloat, [DataType.UINT32]: getInt32InvlerpImpl(DataType.UINT32), [DataType.INT32]: getInt32InvlerpImpl(DataType.INT32), [DataType.UINT64]: [ @@ -247,18 +256,28 @@ function defineLerpUniforms( } export function defineInvlerpShaderFunction( - builder: ShaderBuilder, name: string, dataType: DataType, clamp = false): ShaderCodePart { - return [ - dataTypeShaderDefinition[dataType], - defineLerpUniforms(builder, name, dataType), - glsl_dataTypeComputeInvlerp[dataType], - ` -float ${name}(${getShaderType(dataType)} inputValue) { + builder: ShaderBuilder, name: string, dataType: DataType, clamp = false): ShaderCodePart { + const shaderType = getShaderType(dataType); + let code = ` +float ${name}(${shaderType} inputValue) { float v = computeInvlerp(inputValue, uLerpParams_${name}); ${!clamp ? '' : 'v = clamp(v, 0.0, 1.0);'} return v; } -`, +`; + if (dataType !== DataType.UINT64 && dataType !== DataType.FLOAT32) { + const scalarType = DATA_TYPE_SIGNED[dataType] ? 'int' : 'uint'; + code += ` +float ${name}(${scalarType} inputValue) { + return ${name}(${shaderType}(inputValue)); +} +`; + } + return [ + dataTypeShaderDefinition[dataType], + defineLerpUniforms(builder, name, dataType), + glsl_dataTypeComputeInvlerp[dataType], + code, ]; } diff --git a/src/neuroglancer/webgl/shader_ui_controls.ts b/src/neuroglancer/webgl/shader_ui_controls.ts index 58093f96ad..3a365b1334 100644 --- a/src/neuroglancer/webgl/shader_ui_controls.ts +++ b/src/neuroglancer/webgl/shader_ui_controls.ts @@ -22,8 +22,8 @@ import {parseRGBColorSpecification, TrackableRGB} from 'neuroglancer/util/color' import {DataType} from 'neuroglancer/util/data_type'; import {RefCounted} from 'neuroglancer/util/disposable'; import {vec3} from 'neuroglancer/util/geom'; -import {parseFixedLengthArray, verifyFiniteFloat, verifyInt, verifyObject, verifyOptionalObjectProperty} from 'neuroglancer/util/json'; -import {DataTypeInterval, dataTypeIntervalToJson, defaultDataTypeRange, normalizeDataTypeInterval, parseDataTypeInterval, validateDataTypeInterval} from 'neuroglancer/util/lerp'; +import {parseFixedLengthArray, verifyFiniteFloat, verifyInt, verifyObject, verifyOptionalObjectProperty, verifyString} from 'neuroglancer/util/json'; +import {convertDataTypeInterval, DataTypeInterval, dataTypeIntervalToJson, defaultDataTypeRange, normalizeDataTypeInterval, parseDataTypeInterval, parseUnknownDataTypeInterval, validateDataTypeInterval} from 'neuroglancer/util/lerp'; import {NullarySignal} from 'neuroglancer/util/signal'; import {Trackable} from 'neuroglancer/util/trackable'; import {GL} from 'neuroglancer/webgl/context'; @@ -47,11 +47,20 @@ export interface ShaderColorControl { default: vec3; } -export interface ShaderInvlerpControl { - type: 'invlerp'; +export interface ShaderImageInvlerpControl { + type: 'imageInvlerp'; dataType: DataType; clamp: boolean; - default: InvlerpParameters; + default: ImageInvlerpParameters; +} + +export type PropertiesSpecification = Map; + +export interface ShaderPropertyInvlerpControl { + type: 'propertyInvlerp'; + clamp: boolean; + properties: PropertiesSpecification; + default: PropertyInvlerpParameters; } export interface ShaderCheckboxControl { @@ -61,7 +70,7 @@ export interface ShaderCheckboxControl { } export type ShaderUiControl = - ShaderSliderControl|ShaderColorControl|ShaderInvlerpControl|ShaderCheckboxControl; + ShaderSliderControl|ShaderColorControl|ShaderImageInvlerpControl|ShaderPropertyInvlerpControl|ShaderCheckboxControl; export interface ShaderControlParseError { line: number; @@ -81,6 +90,7 @@ export interface ShaderControlsBuilderState { key: string; parseResult: ShaderControlsParseResult; builderValues: ShaderBuilderValues; + referencedProperties: string[]; } // Strips comments from GLSL code. Also handles string literals since they are used in ui control @@ -338,12 +348,21 @@ function parseInvlerpChannel(value: unknown, rank: number) { function parseInvlerpDirective( valueType: string, parameters: DirectiveParameters, dataContext: ShaderDataContext): DirectiveParseResult { - let errors = []; - const {imageData} = dataContext; - if (imageData === undefined) { - errors.push('invlerp control not supported'); - return {errors}; + const {imageData, properties} = dataContext; + if (imageData !== undefined) { + return parseImageInvlerpDirective(valueType, parameters, imageData); + } + if (properties !== undefined) { + return parsePropertyInvlerpDirective(valueType, parameters, properties); } + let errors = []; + errors.push('invlerp control not supported'); + return {errors}; +} + +function parseImageInvlerpDirective( + valueType: string, parameters: DirectiveParameters, imageData: ImageDataSpecification) { + let errors = []; if (valueType !== 'invlerp') { errors.push('type must be invlerp'); } @@ -352,6 +371,7 @@ function parseInvlerpDirective( let clamp = true; let range = defaultDataTypeRange[dataType]; let window: DataTypeInterval|undefined; + let property: string|undefined; for (let [key, value] of parameters) { try { switch (key) { @@ -388,11 +408,83 @@ function parseInvlerpDirective( } return { control: { - type: 'invlerp', + type: 'imageInvlerp', dataType, clamp, - default: {range, window: window ?? normalizeDataTypeInterval(range), channel}, - } as ShaderInvlerpControl, + default: {range, window: window ?? normalizeDataTypeInterval(range), channel, property}, + } as ShaderImageInvlerpControl, + errors: undefined, + }; +} + +function parsePropertyInvlerpDirective( + valueType: string, parameters: DirectiveParameters, properties: Map) { + let errors = []; + if (valueType !== 'invlerp') { + errors.push('type must be invlerp'); + } + let clamp = true; + let range: any; + let window: any; + let property: string|undefined; + for (let [key, value] of parameters) { + try { + switch (key) { + case 'range': { + range = parseUnknownDataTypeInterval(value); + break; + } + case 'window': { + window = parseUnknownDataTypeInterval(value); + break; + } + case 'clamp': { + if (typeof value !== 'boolean') { + errors.push(`Invalid clamp value: ${JSON.stringify(value)}`); + } else { + clamp = value; + } + break; + } + case 'property': { + const s = verifyString(value); + if (!properties.has(s)) { + throw new Error(`Property not defined: ${JSON.stringify(property)}`); + } + property = s; + break; + } + default: + errors.push(`Invalid parameter: ${key}`); + break; + } + } catch (e) { + errors.push(`Invalid ${key} value: ${e.message}`); + } + } + if (errors.length > 0) { + return {errors}; + } + if (property === undefined) { + for (const p of properties.keys()) { + property = p; + break; + } + } + const dataType = properties.get(property!)!; + if (range !== undefined) { + range = convertDataTypeInterval(range, dataType); + } + if (window !== undefined) { + window = convertDataTypeInterval(window, dataType); + } + return { + control: { + type: 'propertyInvlerp', + clamp, + properties, + default: {range, window, property, dataType}, + } as ShaderPropertyInvlerpControl, errors: undefined, }; } @@ -404,6 +496,7 @@ export interface ImageDataSpecification { export interface ShaderDataContext { imageData?: ImageDataSpecification; + properties?: Map; } const controlParsers = new Map< @@ -484,7 +577,7 @@ export function addControlsToBuilder( const uName = uniformName(name); const builderValue = builderValues[name]; switch (control.type) { - case 'invlerp': { + case 'imageInvlerp': { const code = [ defineInvlerpShaderFunction(builder, uName, control.dataType, control.clamp), ` float ${uName}() { @@ -496,6 +589,20 @@ float ${uName}() { builder.addFragmentCode(`#define ${name} ${uName}\n`); break; } + case 'propertyInvlerp': { + const property = builderValue.property; + const dataType = control.properties.get(property)!; + const code = [ + defineInvlerpShaderFunction(builder, uName, dataType, control.clamp), ` +float ${uName}() { + return ${uName}(prop_${property}()); +} +` + ]; + builder.addVertexCode(code); + builder.addVertexCode(`#define ${name} ${uName}\n`); + break; + } case 'checkbox': { const code = `#define ${name} ${builderValue.value}\n`; builder.addFragmentCode(code); @@ -543,11 +650,22 @@ export class WatchableShaderUiControls implements WatchableValueInterface { - constructor(public dataType: DataType, public defaultValue: InvlerpParameters) { - super(defaultValue, obj => parseInvlerpParameters(obj, dataType, defaultValue)); +class TrackableImageInvlerpParameters extends TrackableValue { + constructor(public dataType: DataType, public defaultValue: ImageInvlerpParameters) { + super(defaultValue, obj => parseImageInvlerpParameters(obj, dataType, defaultValue)); } toJSON() { @@ -579,6 +697,51 @@ class TrackableInvlerpParameters extends TrackableValue { } } +function parsePropertyInvlerpParameters( + obj: unknown, properties: PropertiesSpecification, + defaultValue: PropertyInvlerpParameters): PropertyInvlerpParameters { + if (obj === undefined) return defaultValue; + verifyObject(obj); + const property = + verifyOptionalObjectProperty(obj, 'property', property => { + property = verifyString(property); + if (!properties.has(property)) { + throw new Error(`Invalid value: ${JSON.stringify(property)}`); + } + return property; + }, defaultValue.property); + const dataType = properties.get(property)!; + return { + property, + dataType, + range: verifyOptionalObjectProperty( + obj, 'range', x => parseDataTypeInterval(x, dataType), defaultValue.range), + window: verifyOptionalObjectProperty( + obj, 'window', x => validateDataTypeInterval(parseDataTypeInterval(x, dataType)), + defaultValue.window), + }; +} + +class TrackablePropertyInvlerpParameters extends TrackableValue { + constructor(public properties: PropertiesSpecification, public defaultValue: PropertyInvlerpParameters) { + super(defaultValue, obj => parsePropertyInvlerpParameters(obj, properties, defaultValue)); + } + + toJSON() { + const {value: {range, window, property, dataType}, defaultValue} = this; + const defaultRange = defaultDataTypeRange[dataType]; + const rangeJson = + dataTypeIntervalToJson(range ?? defaultRange, dataType, defaultValue.range ?? defaultRange); + const windowJson = dataTypeIntervalToJson( + window ?? defaultRange, dataType, defaultValue.window ?? defaultRange); + const propertyJson = property === defaultValue.property ? undefined : property; + if (rangeJson === undefined && windowJson === undefined && propertyJson === undefined) { + return undefined; + } + return {range: rangeJson, window: windowJson, property: propertyJson}; + } +} + function getControlTrackable(control: ShaderUiControl): {trackable: TrackableValueInterface, getBuilderValue: (value: any) => any} { switch (control.type) { @@ -603,12 +766,18 @@ function getControlTrackable(control: ShaderUiControl): }; case 'color': return {trackable: new TrackableRGB(control.default), getBuilderValue: () => null}; - case 'invlerp': + case 'imageInvlerp': return { - trackable: new TrackableInvlerpParameters(control.dataType, control.default), - getBuilderValue: (value: InvlerpParameters) => + trackable: new TrackableImageInvlerpParameters(control.dataType, control.default), + getBuilderValue: (value: ImageInvlerpParameters) => ({channel: value.channel, dataType: control.dataType}), }; + case 'propertyInvlerp': + return { + trackable: new TrackablePropertyInvlerpParameters(control.properties, control.default), + getBuilderValue: (value: PropertyInvlerpParameters) => + ({property: value.property, dataType: value.dataType}), + }; case 'checkbox': return { trackable: new TrackableBoolean(control.default), @@ -637,11 +806,21 @@ function encodeBuilderStateKey( export function getFallbackBuilderState(parseResult: ShaderControlsParseResult): ShaderControlsBuilderState { const builderValues: ShaderBuilderValues = {}; + const referencedProperties = []; for (const [key, control] of parseResult.controls) { const {trackable, getBuilderValue} = getControlTrackable(control); - builderValues[key] = getBuilderValue(trackable.value); + const builderValue = getBuilderValue(trackable.value); + builderValues[key] = builderValue; + if (control.type === 'propertyInvlerp') { + referencedProperties.push(builderValue.property); + } } - return {builderValues, parseResult, key: encodeBuilderStateKey(builderValues, parseResult)}; + return { + builderValues, + parseResult, + key: encodeBuilderStateKey(builderValues, parseResult), + referencedProperties, + }; } export class ShaderControlState extends RefCounted implements @@ -696,14 +875,19 @@ export class ShaderControlState extends RefCounted implements this.builderState = makeCachedDerivedWatchableValue( (parseResult: ShaderControlsParseResult, state: ShaderControlMap) => { const builderValues: ShaderBuilderValues = {}; - for (const [key, {trackable, getBuilderValue}] of state) { + const referencedProperties = []; + for (const [key, {control, trackable, getBuilderValue}] of state) { const builderValue = getBuilderValue(trackable.value); builderValues[key] = builderValue; + if (control.type === 'propertyInvlerp') { + referencedProperties.push(builderValue.property); + } } return { key: encodeBuilderStateKey(builderValues, parseResult), parseResult, - builderValues + builderValues, + referencedProperties, }; }, [this.parseResult, this], (a, b) => a.key === b.key); @@ -711,23 +895,35 @@ export class ShaderControlState extends RefCounted implements state => { const channels: HistogramChannelSpecification[] = []; for (const {control, trackable} of state.values()) { - if (control.type !== 'invlerp') continue; + if (control.type !== 'imageInvlerp') continue; channels.push({channel: trackable.value.channel}); } return channels; }, [this], (a, b) => arraysEqualWithPredicate(a, b, (ca, cb) => arraysEqual(ca.channel, cb.channel))); + const histogramProperties = makeCachedDerivedWatchableValue(state => { + const properties: string[] = []; + for (const {control, trackable} of state.values()) { + if (control.type !== 'propertyInvlerp') continue; + properties.push(trackable.value.property); + } + return properties; + }, [this], arraysEqual); const histogramBounds = makeCachedLazyDerivedWatchableValue(state => { const bounds: DataTypeInterval[] = []; for (const {control, trackable} of state.values()) { - if (control.type !== 'invlerp') continue; - bounds.push(trackable.value.window); + if (control.type === 'imageInvlerp') { + bounds.push(trackable.value.window); + } else if (control.type === 'propertyInvlerp') { + const {dataType, range, window} = trackable.value as PropertyInvlerpParameters; + bounds.push(window ?? range ?? defaultDataTypeRange[dataType]); + } } return bounds; }, this); - this.histogramSpecifications = - this.registerDisposer(new HistogramSpecifications(histogramChannels, histogramBounds)); + this.histogramSpecifications = this.registerDisposer( + new HistogramSpecifications(histogramChannels, histogramProperties, histogramBounds)); } private handleFragmentMainChanged() { @@ -902,9 +1098,15 @@ function setControlInShader( case 'color': gl.uniform3fv(uniform, value); break; - case 'invlerp': + case 'imageInvlerp': enableLerpShaderFunction(shader, uName, control.dataType, value.range); break; + case 'propertyInvlerp': { + const {dataType} = value as PropertyInvlerpParameters; + enableLerpShaderFunction( + shader, uName, dataType, value.range ?? defaultDataTypeRange[dataType]); + break; + } case 'checkbox': // Value is hard-coded in shader. break; diff --git a/src/neuroglancer/widget/invlerp.ts b/src/neuroglancer/widget/invlerp.ts index 2d4d544dbc..5dbe9376ad 100644 --- a/src/neuroglancer/widget/invlerp.ts +++ b/src/neuroglancer/widget/invlerp.ts @@ -20,12 +20,13 @@ import svg_arrowLeft from 'ikonate/icons/arrow-left.svg'; import svg_arrowRight from 'ikonate/icons/arrow-right.svg'; import {DisplayContext, IndirectRenderedPanel} from 'neuroglancer/display_context'; import {WatchableValueInterface} from 'neuroglancer/trackable_value'; +import {ToolActivation} from 'neuroglancer/ui/tool'; import {animationFrameDebounce} from 'neuroglancer/util/animation_frame_debounce'; import {DataType} from 'neuroglancer/util/data_type'; -import {RefCounted} from 'neuroglancer/util/disposable'; -import {updateInputFieldWidth} from 'neuroglancer/util/dom'; +import {Owned, RefCounted} from 'neuroglancer/util/disposable'; +import {removeChildren, updateInputFieldWidth} from 'neuroglancer/util/dom'; import {EventActionMap, registerActionListener} from 'neuroglancer/util/event_action_map'; -import {computeInvlerp, computeLerp, dataTypeCompare, DataTypeInterval, getClampedInterval, getClosestEndpoint, getIntervalBoundsEffectiveFraction, getIntervalBoundsEffectiveOffset, parseDataTypeValue} from 'neuroglancer/util/lerp'; +import {computeInvlerp, computeLerp, dataTypeCompare, DataTypeInterval, dataTypeIntervalEqual, getClampedInterval, getClosestEndpoint, getIntervalBoundsEffectiveFraction, getIntervalBoundsEffectiveOffset, parseDataTypeValue} from 'neuroglancer/util/lerp'; import {MouseEventBinder} from 'neuroglancer/util/mouse_bindings'; import {startRelativeMouseDrag} from 'neuroglancer/util/mouse_drag'; import {Uint64} from 'neuroglancer/util/uint64'; @@ -42,6 +43,7 @@ import {InvlerpParameters} from 'neuroglancer/webgl/shader_ui_controls'; import {getSquareCornersBuffer} from 'neuroglancer/webgl/square_corners_buffer'; import {setRawTextureParameters} from 'neuroglancer/webgl/texture'; import {makeIcon} from 'neuroglancer/widget/icon'; +import {LayerControlTool} from 'neuroglancer/widget/layer_control'; import {LegendShaderOptions} from 'neuroglancer/widget/shader_controls'; import {Tab} from 'neuroglancer/widget/tab_view'; @@ -575,3 +577,83 @@ export class InvlerpWidget extends Tab { invertArrows[reversed ? 0 : 1].style.display = 'none'; } } + +export class VariableDataTypeInvlerpWidget extends Tab { + invlerpWidget: Owned; + constructor( + visibility: WatchableVisibilityPriority, public display: DisplayContext, + public watchableDataType: WatchableValueInterface, + public trackable: WatchableValueInterface, + public histogramSpecifications: HistogramSpecifications, public histogramIndex: number, + public legendShaderOptions: LegendShaderOptions|undefined) { + super(visibility); + this.invlerpWidget = this.makeInvlerpWidget(); + this.registerDisposer(watchableDataType.changed.add(() => { + removeChildren(this.element); + this.invlerpWidget.dispose(); + this.invlerpWidget = this.makeInvlerpWidget(); + })); + } + + get dataType() { + return this.watchableDataType.value; + } + + disposed() { + this.invlerpWidget.dispose(); + super.disposed(); + } + + private makeInvlerpWidget() { + const {dataType} = this; + const widget = new InvlerpWidget( + this.visibility, this.display, dataType, this.trackable, this.histogramSpecifications, + this.histogramIndex, this.legendShaderOptions); + this.element.appendChild(widget.element); + return widget; + } +} + +const TOOL_INPUT_EVENT_MAP = EventActionMap.fromObject({ + 'at:shift+wheel': {action: 'adjust-contrast-via-wheel'}, + 'at:shift+mousedown0': {action: 'adjust-via-drag'}, + 'at:shift+mousedown2': {action: 'invert-range'}, +}); + +export function activateInvlerpTool( + activation: ToolActivation, + control: InvlerpWidget|VariableDataTypeInvlerpWidget) { + activation.bindInputEventMap(TOOL_INPUT_EVENT_MAP); + activation.bindAction('adjust-contrast-via-wheel', event => { + event.stopPropagation(); + const zoomAmount = getWheelZoomAmount(event.detail); + adjustInvlerpContrast(control.dataType, control.trackable, zoomAmount); + }); + activation.bindAction('adjust-via-drag', event => { + event.stopPropagation(); + let baseScreenX = event.detail.screenX, baseScreenY = event.detail.screenY; + let baseRange = control.trackable.value.range; + let prevRange = baseRange; + let prevScreenX = baseScreenX, prevScreenY = baseScreenY; + startRelativeMouseDrag(event.detail, newEvent => { + const curRange = control.trackable.value.range; + const curScreenX = newEvent.screenX, curScreenY = newEvent.screenY; + if (!dataTypeIntervalEqual(control.dataType, curRange, prevRange)) { + baseRange = curRange; + baseScreenX = prevScreenX; + baseScreenY = prevScreenY; + } + adjustInvlerpBrightnessContrast( + control.dataType, control.trackable, baseRange, + (curScreenY - baseScreenY) * 2 / screen.height, + (curScreenX - baseScreenX) * 4 / screen.width); + prevRange = control.trackable.value.range; + prevScreenX = curScreenX; + prevScreenY = curScreenY; + }); + }); + activation.bindAction('invert-range', event => { + event.stopPropagation(); + invertInvlerpRange(control.trackable); + }); +} diff --git a/src/neuroglancer/widget/layer_control_channel_invlerp.ts b/src/neuroglancer/widget/layer_control_channel_invlerp.ts index 976a176bbd..7721808fe2 100644 --- a/src/neuroglancer/widget/layer_control_channel_invlerp.ts +++ b/src/neuroglancer/widget/layer_control_channel_invlerp.ts @@ -20,26 +20,16 @@ import {Position} from 'neuroglancer/navigation_state'; import {WatchableValueInterface} from 'neuroglancer/trackable_value'; import {arraysEqual} from 'neuroglancer/util/array'; import {DataType} from 'neuroglancer/util/data_type'; -import {EventActionMap} from 'neuroglancer/util/event_action_map'; -import {dataTypeIntervalEqual} from 'neuroglancer/util/lerp'; -import {startRelativeMouseDrag} from 'neuroglancer/util/mouse_drag'; -import {getWheelZoomAmount} from 'neuroglancer/util/wheel_zoom'; import {HistogramSpecifications} from 'neuroglancer/webgl/empirical_cdf'; -import {InvlerpParameters} from 'neuroglancer/webgl/shader_ui_controls'; -import {adjustInvlerpBrightnessContrast, adjustInvlerpContrast, invertInvlerpRange, InvlerpWidget} from 'neuroglancer/widget/invlerp'; +import {ImageInvlerpParameters} from 'neuroglancer/webgl/shader_ui_controls'; +import {activateInvlerpTool, InvlerpWidget} from 'neuroglancer/widget/invlerp'; import {LayerControlFactory} from 'neuroglancer/widget/layer_control'; import {PositionWidget} from 'neuroglancer/widget/position_widget'; import {LegendShaderOptions} from 'neuroglancer/widget/shader_controls'; -const TOOL_INPUT_EVENT_MAP = EventActionMap.fromObject({ - 'at:shift+wheel': {action: 'adjust-contrast-via-wheel'}, - 'at:shift+mousedown0': {action: 'adjust-via-drag'}, - 'at:shift+mousedown2': {action: 'invert-range'}, -}); - export function channelInvlerpLayerControl( getter: (layer: LayerType) => { - watchableValue: WatchableValueInterface, + watchableValue: WatchableValueInterface, dataType: DataType, defaultChannel: number[], channelCoordinateSpaceCombiner: CoordinateSpaceCombiner | undefined, @@ -89,39 +79,7 @@ export function channelInvlerpLayerControl( return {control, controlElement: control.element}; }, activateTool: (activation, control) => { - activation.bindInputEventMap(TOOL_INPUT_EVENT_MAP); - activation.bindAction('adjust-contrast-via-wheel', event => { - event.stopPropagation(); - const zoomAmount = getWheelZoomAmount(event.detail); - adjustInvlerpContrast(control.dataType, control.trackable, zoomAmount); - }); - activation.bindAction('adjust-via-drag', event => { - event.stopPropagation(); - let baseScreenX = event.detail.screenX, baseScreenY = event.detail.screenY; - let baseRange = control.trackable.value.range; - let prevRange = baseRange; - let prevScreenX = baseScreenX, prevScreenY = baseScreenY; - startRelativeMouseDrag(event.detail, newEvent => { - const curRange = control.trackable.value.range; - const curScreenX = newEvent.screenX, curScreenY = newEvent.screenY; - if (!dataTypeIntervalEqual(control.dataType, curRange, prevRange)) { - baseRange = curRange; - baseScreenX = prevScreenX; - baseScreenY = prevScreenY; - } - adjustInvlerpBrightnessContrast( - control.dataType, control.trackable, baseRange, - (curScreenY - baseScreenY) * 2 / screen.height, - (curScreenX - baseScreenX) * 4 / screen.width); - prevRange = control.trackable.value.range; - prevScreenX = curScreenX; - prevScreenY = curScreenY; - }); - }); - activation.bindAction('invert-range', event => { - event.stopPropagation(); - invertInvlerpRange(control.trackable); - }); + activateInvlerpTool(activation, control); }, }; } diff --git a/src/neuroglancer/widget/layer_control_property_invlerp.ts b/src/neuroglancer/widget/layer_control_property_invlerp.ts new file mode 100644 index 0000000000..412debb64a --- /dev/null +++ b/src/neuroglancer/widget/layer_control_property_invlerp.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2019 Google Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {UserLayer} from 'neuroglancer/layer'; +import {makeCachedDerivedWatchableValue, WatchableValueInterface} from 'neuroglancer/trackable_value'; +import {DataType} from 'neuroglancer/util/data_type'; +import {convertDataTypeInterval, defaultDataTypeRange, normalizeDataTypeInterval} from 'neuroglancer/util/lerp'; +import {HistogramSpecifications} from 'neuroglancer/webgl/empirical_cdf'; +import {InvlerpParameters, PropertiesSpecification, PropertyInvlerpParameters} from 'neuroglancer/webgl/shader_ui_controls'; +import {activateInvlerpTool, VariableDataTypeInvlerpWidget} from 'neuroglancer/widget/invlerp'; +import {LayerControlFactory} from 'neuroglancer/widget/layer_control'; +import {LegendShaderOptions} from 'neuroglancer/widget/shader_controls'; + +export function propertyInvlerpLayerControl( + getter: (layer: LayerType) => { + watchableValue: WatchableValueInterface, + properties: PropertiesSpecification, + histogramSpecifications: HistogramSpecifications, + histogramIndex: number, + legendShaderOptions: LegendShaderOptions | undefined, + }): LayerControlFactory { + return { + makeControl: (layer, context, options) => { + const { + watchableValue, + properties, + histogramSpecifications, + legendShaderOptions, + histogramIndex + } = getter(layer); + { + const propertySelectElement = document.createElement('select'); + for (const [property, dataType] of properties) { + const optionElement = document.createElement('option'); + optionElement.textContent = `${property} (${DataType[dataType].toLowerCase()})`; + optionElement.value = property; + propertySelectElement.appendChild(optionElement); + } + const updateModel = () => { + const property = propertySelectElement.value; + const dataType = properties.get(property)!; + const {window, range} = watchableValue.value; + watchableValue.value = { + window: window !== undefined ? convertDataTypeInterval(window, dataType) : undefined, + range: range !== undefined ? convertDataTypeInterval(range, dataType) : undefined, + property, + dataType + }; + }; + const updateView = () => { + propertySelectElement.value = watchableValue.value.property; + }; + context.registerEventListener(propertySelectElement, 'change', updateModel); + context.registerDisposer(watchableValue.changed.add(updateView)); + updateView(); + options.labelContainer.appendChild(propertySelectElement); + } + const derivedWatchableValue = { + changed: watchableValue.changed, + get value(): InvlerpParameters { + let {dataType, window, range} = watchableValue.value; + if (range === undefined) { + range = defaultDataTypeRange[dataType]; + } + return { + window: normalizeDataTypeInterval(window ?? range), + range, + }; + }, + set value(newValue: InvlerpParameters) { + const {window, range} = newValue; + watchableValue.value = {...watchableValue.value, window, range}; + } + }; + const derivedDataTypeWatchable = makeCachedDerivedWatchableValue(p => p.dataType, [watchableValue]); + const control = context.registerDisposer(new VariableDataTypeInvlerpWidget( + options.visibility, options.display, derivedDataTypeWatchable, derivedWatchableValue, + histogramSpecifications, histogramIndex, legendShaderOptions)); + return {control, controlElement: control.element}; + }, + activateTool: (activation, control) => { + activateInvlerpTool(activation, control); + }, + }; +} diff --git a/src/neuroglancer/widget/shader_controls.ts b/src/neuroglancer/widget/shader_controls.ts index e68244ff44..1460aebff7 100644 --- a/src/neuroglancer/widget/shader_controls.ts +++ b/src/neuroglancer/widget/shader_controls.ts @@ -29,6 +29,7 @@ import {addLayerControlToOptionsTab, LayerControlDefinition, LayerControlFactory import {channelInvlerpLayerControl} from 'neuroglancer/widget/layer_control_channel_invlerp'; import {checkboxLayerControl} from 'neuroglancer/widget/layer_control_checkbox'; import {colorLayerControl} from 'neuroglancer/widget/layer_control_color'; +import {propertyInvlerpLayerControl} from 'neuroglancer/widget/layer_control_property_invlerp'; import {rangeLayerControl} from 'neuroglancer/widget/layer_control_range'; import {Tab} from 'neuroglancer/widget/tab_view'; @@ -59,11 +60,11 @@ function getShaderLayerControlFactory( return colorLayerControl(() => controlState.trackable); case 'checkbox': return checkboxLayerControl(() => controlState.trackable); - case 'invlerp': { + case 'imageInvlerp': { let histogramIndex = 0; for (const [otherName, {control: {type: otherType}}] of shaderControlState.state) { if (otherName === controlId) break; - if (otherType === 'invlerp') ++histogramIndex; + if (otherType === 'imageInvlerp') ++histogramIndex; } return channelInvlerpLayerControl( () => ({ @@ -76,6 +77,21 @@ function getShaderLayerControlFactory( legendShaderOptions: layerShaderControls.legendShaderOptions, })); } + case 'propertyInvlerp': { + let histogramIndex = 0; + for (const [otherName, {control: {type: otherType}}] of shaderControlState.state) { + if (otherName === controlId) break; + if (otherType === 'propertyInvlerp') ++histogramIndex; + } + return propertyInvlerpLayerControl( + () => ({ + properties: control.properties, + watchableValue: controlState.trackable, + histogramSpecifications: shaderControlState.histogramSpecifications, + histogramIndex, + legendShaderOptions: layerShaderControls.legendShaderOptions, + })); + } } }