Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(annotation) added annotation tag type properties and annotation iteration keybinds #650

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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