Skip to content

Commit

Permalink
added annotation tagging feature
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisj committed Oct 3, 2024
1 parent f574c02 commit 02e0a71
Show file tree
Hide file tree
Showing 10 changed files with 654 additions and 80 deletions.
2 changes: 1 addition & 1 deletion src/annotation/annotation_layer_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ void main() {

export class AnnotationDisplayState extends RefCounted {
annotationProperties = new WatchableValue<
AnnotationPropertySpec[] | undefined
readonly Readonly<AnnotationPropertySpec>[] | undefined
>(undefined);
shader = makeTrackableFragmentMain(DEFAULT_FRAGMENT_MAIN);
shaderControls = new ShaderControlState(
Expand Down
9 changes: 6 additions & 3 deletions src/annotation/frontend_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
SliceViewChunkSource,
} from "#src/sliceview/frontend.js";
import { StatusMessage } from "#src/status.js";
import { WatchableValue } from "#src/trackable_value.js";
import type { Borrowed, Owned } from "#src/util/disposable.js";
import { ENDIANNESS, Endianness } from "#src/util/endian.js";
import * as matrix from "#src/util/matrix.js";
Expand Down Expand Up @@ -517,7 +518,9 @@ export class MultiscaleAnnotationSource
spatiallyIndexedSources = new Set<Borrowed<AnnotationGeometryChunkSource>>();
rank: number;
readonly relationships: readonly string[];
readonly properties: Readonly<AnnotationPropertySpec>[];
readonly properties: WatchableValue<
readonly Readonly<AnnotationPropertySpec>[]
>;
readonly annotationPropertySerializers: AnnotationPropertySerializer[];
constructor(
public chunkManager: Borrowed<ChunkManager>,
Expand All @@ -529,10 +532,10 @@ export class MultiscaleAnnotationSource
) {
super();
this.rank = options.rank;
this.properties = options.properties;
this.properties = new WatchableValue(options.properties);
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
this.rank,
this.properties,
this.properties.value,
);
const segmentFilteredSources: Owned<AnnotationSubsetGeometryChunkSource>[] =
(this.segmentFilteredSources = []);
Expand Down
93 changes: 81 additions & 12 deletions src/annotation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
CoordinateSpaceTransform,
WatchableCoordinateSpaceTransform,
} from "#src/coordinate_transform.js";
import { WatchableValue } from "#src/trackable_value.js";
import { arraysEqual } from "#src/util/array.js";
import {
packColor,
Expand Down Expand Up @@ -106,6 +107,13 @@ export interface AnnotationNumericPropertySpec
min?: number;
max?: number;
step?: number;
tag?: string;
}

export interface AnnotationTagPropertySpec
extends AnnotationNumericPropertySpec {
type: "int8";
tag: string;
}

export const propertyTypeDataType: Record<
Expand All @@ -127,6 +135,18 @@ export type AnnotationPropertySpec =
| AnnotationColorPropertySpec
| AnnotationNumericPropertySpec;

export function isAnnotationNumericPropertySpec(
spec: AnnotationPropertySpec,
): spec is AnnotationNumericPropertySpec {
return spec.type !== "rgb" && spec.type !== "rgba";
}

export function isAnnotationTagPropertySpec(
spec: AnnotationPropertySpec,
): spec is AnnotationTagPropertySpec {
return spec.type === "uint8" && spec.tag !== undefined;
}

export interface AnnotationPropertyTypeHandler {
serializedBytes(rank: number): number;
alignment(rank: number): number;
Expand Down Expand Up @@ -569,6 +589,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
);
let enumValues: number[] | undefined;
let enumLabels: string[] | undefined;
let tag: string | undefined;
switch (type) {
case "rgb":
case "rgba":
Expand All @@ -593,6 +614,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
),
);
}
tag = verifyOptionalObjectProperty(obj, "tag", verifyString);
}
}
return {
Expand All @@ -602,15 +624,23 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
default: defaultValue,
enumValues,
enumLabels,
tag,
} as AnnotationPropertySpec;
}

function annotationPropertySpecToJson(spec: AnnotationPropertySpec) {
const defaultValue = spec.default;
const isNumeric = isAnnotationNumericPropertySpec(spec);
const tag = isNumeric ? spec.tag : undefined;
const enum_values = isNumeric ? spec.enumValues : undefined;
const enum_labels = isNumeric ? spec.enumLabels : undefined;
return {
id: spec.identifier,
description: spec.description,
type: spec.type,
tag,
enum_values,
enum_labels,
default:
defaultValue === 0
? undefined
Expand Down Expand Up @@ -1000,7 +1030,7 @@ export const annotationTypeHandlers: Record<
export interface AnnotationSchema {
rank: number;
relationships: readonly string[];
properties: readonly AnnotationPropertySpec[];
properties: WatchableValue<readonly Readonly<AnnotationPropertySpec>[]>;
}

export function annotationToJson(
Expand All @@ -1020,8 +1050,8 @@ export function annotationToJson(
segments.map((x) => x.toString()),
);
}
if (schema.properties.length !== 0) {
const propertySpecs = schema.properties;
const propertySpecs = schema.properties.value;
if (propertySpecs.length !== 0) {
result.props = annotation.properties.map((prop, i) =>
annotationPropertyTypeHandlers[propertySpecs[i].type].serializeJson(prop),
);
Expand Down Expand Up @@ -1061,9 +1091,9 @@ function restoreAnnotation(
);
});
const properties = verifyObjectProperty(obj, "props", (propsObj) => {
const propSpecs = schema.properties;
const propSpecs = schema.properties.value;
if (propsObj === undefined) return propSpecs.map((x) => x.default);
return parseArray(expectArray(propsObj, schema.properties.length), (x, i) =>
return parseArray(expectArray(propsObj, propSpecs.length), (x, i) =>
annotationPropertyTypeHandlers[propSpecs[i].type].deserializeJson(x),
);
});
Expand Down Expand Up @@ -1111,13 +1141,15 @@ export class AnnotationSource
constructor(
rank: number,
public readonly relationships: readonly string[] = [],
public readonly properties: Readonly<AnnotationPropertySpec>[] = [],
public readonly properties: WatchableValue<
readonly Readonly<AnnotationPropertySpec>[]
> = new WatchableValue([]),
) {
super();
this.rank_ = rank;
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
rank,
properties,
properties.value,
);
}

Expand Down Expand Up @@ -1261,16 +1293,56 @@ export class LocalAnnotationSource extends AnnotationSource {

constructor(
public watchableTransform: WatchableCoordinateSpaceTransform,
properties: AnnotationPropertySpec[],
public readonly properties: WatchableValue<
AnnotationPropertySpec[]
> = new WatchableValue([]),
relationships: string[],
) {
super(watchableTransform.value.sourceRank, relationships, properties);
this.curCoordinateTransform = watchableTransform.value;
this.registerDisposer(
watchableTransform.changed.add(() => this.ensureUpdated()),
);

this.registerDisposer(
properties.changed.add(() => {
this.updateAnnotationPropertySerializers();
this.changed.dispatch();
}),
);
}

updateAnnotationPropertySerializers() {
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
this.rank_,
this.properties.value,
);
}

addProperty(property: AnnotationPropertySpec) {
this.properties.value.push(property);
for (const annotation of this) {
annotation.properties.push(property.default);
}
this.properties.changed.dispatch();
}

removeProperty(identifier: string) {
const propertyIndex = this.properties.value.findIndex(
(x) => x.identifier === identifier,
);
this.properties.value.splice(propertyIndex, 1);
for (const annotation of this) {
annotation.properties.splice(propertyIndex, 1);
}
this.properties.changed.dispatch();
}

getTagProperties = () => {
const { properties } = this;
return properties.value.filter(isAnnotationTagPropertySpec);
};

ensureUpdated() {
const transform = this.watchableTransform.value;
const { curCoordinateTransform } = this;
Expand Down Expand Up @@ -1325,10 +1397,7 @@ export class LocalAnnotationSource extends AnnotationSource {
}
if (this.rank_ !== sourceRank) {
this.rank_ = sourceRank;
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
this.rank_,
this.properties,
);
this.updateAnnotationPropertySerializers();
}
this.changed.dispatch();
}
Expand Down
18 changes: 14 additions & 4 deletions src/annotation/renderlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,16 +474,18 @@ function AnnotationRenderLayer<
private renderHelpers: AnnotationRenderHelper[] = [];
private tempChunkPosition: Float32Array;

handleRankChanged() {
handleRankChanged(force = false) {
const { rank } = this.base.source;
if (rank === this.curRank) return;
if (!force && rank === this.curRank) return;
this.curRank = rank;
this.tempChunkPosition = new Float32Array(rank);
const { renderHelpers, gl } = this;
for (const oldHelper of renderHelpers) {
oldHelper.dispose();
}
const { properties } = this.base.source;
const {
properties: { value: properties },
} = this.base.source;
const { displayState } = this.base.state;
for (const annotationType of annotationTypes) {
const handler = getAnnotationTypeRenderHandler(annotationType);
Expand Down Expand Up @@ -522,6 +524,12 @@ function AnnotationRenderLayer<
});
this.role = base.state.role;
this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
this.registerDisposer(
base.source.properties.changed.add(() => {
// todo, does it make sense to run this whole function? Or should we pass the watchable value to renderHelperConstructor?
this.handleRankChanged(true);
}),
);
this.handleRankChanged();
this.registerDisposer(
this.base.state.displayState.shaderControls.histogramSpecifications.producerVisibility.add(
Expand Down Expand Up @@ -780,7 +788,9 @@ function AnnotationRenderLayer<
transformPickedValue(pickState: PickState) {
const { pickedAnnotationBuffer } = pickState;
if (pickedAnnotationBuffer === undefined) return undefined;
const { properties } = this.base.source;
const {
properties: { value: properties },
} = this.base.source;
if (properties.length === 0) return undefined;
const {
pickedAnnotationBufferBaseOffset,
Expand Down
2 changes: 1 addition & 1 deletion src/datasource/graphene/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ function makeColoredAnnotationState(
const { subsourceEntry } = loadedSubsource;
const source = new LocalAnnotationSource(
loadedSubsource.loadedDataSource.transform,
[],
new WatchableValue([]),
["associated segments"],
);

Expand Down
Loading

0 comments on commit 02e0a71

Please sign in to comment.