Skip to content

Commit 02e0a71

Browse files
committed
added annotation tagging feature
1 parent f574c02 commit 02e0a71

File tree

10 files changed

+654
-80
lines changed

10 files changed

+654
-80
lines changed

src/annotation/annotation_layer_state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ void main() {
132132

133133
export class AnnotationDisplayState extends RefCounted {
134134
annotationProperties = new WatchableValue<
135-
AnnotationPropertySpec[] | undefined
135+
readonly Readonly<AnnotationPropertySpec>[] | undefined
136136
>(undefined);
137137
shader = makeTrackableFragmentMain(DEFAULT_FRAGMENT_MAIN);
138138
shaderControls = new ShaderControlState(

src/annotation/frontend_source.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
SliceViewChunkSource,
5757
} from "#src/sliceview/frontend.js";
5858
import { StatusMessage } from "#src/status.js";
59+
import { WatchableValue } from "#src/trackable_value.js";
5960
import type { Borrowed, Owned } from "#src/util/disposable.js";
6061
import { ENDIANNESS, Endianness } from "#src/util/endian.js";
6162
import * as matrix from "#src/util/matrix.js";
@@ -517,7 +518,9 @@ export class MultiscaleAnnotationSource
517518
spatiallyIndexedSources = new Set<Borrowed<AnnotationGeometryChunkSource>>();
518519
rank: number;
519520
readonly relationships: readonly string[];
520-
readonly properties: Readonly<AnnotationPropertySpec>[];
521+
readonly properties: WatchableValue<
522+
readonly Readonly<AnnotationPropertySpec>[]
523+
>;
521524
readonly annotationPropertySerializers: AnnotationPropertySerializer[];
522525
constructor(
523526
public chunkManager: Borrowed<ChunkManager>,
@@ -529,10 +532,10 @@ export class MultiscaleAnnotationSource
529532
) {
530533
super();
531534
this.rank = options.rank;
532-
this.properties = options.properties;
535+
this.properties = new WatchableValue(options.properties);
533536
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
534537
this.rank,
535-
this.properties,
538+
this.properties.value,
536539
);
537540
const segmentFilteredSources: Owned<AnnotationSubsetGeometryChunkSource>[] =
538541
(this.segmentFilteredSources = []);

src/annotation/index.ts

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
CoordinateSpaceTransform,
2424
WatchableCoordinateSpaceTransform,
2525
} from "#src/coordinate_transform.js";
26+
import { WatchableValue } from "#src/trackable_value.js";
2627
import { arraysEqual } from "#src/util/array.js";
2728
import {
2829
packColor,
@@ -106,6 +107,13 @@ export interface AnnotationNumericPropertySpec
106107
min?: number;
107108
max?: number;
108109
step?: number;
110+
tag?: string;
111+
}
112+
113+
export interface AnnotationTagPropertySpec
114+
extends AnnotationNumericPropertySpec {
115+
type: "int8";
116+
tag: string;
109117
}
110118

111119
export const propertyTypeDataType: Record<
@@ -127,6 +135,18 @@ export type AnnotationPropertySpec =
127135
| AnnotationColorPropertySpec
128136
| AnnotationNumericPropertySpec;
129137

138+
export function isAnnotationNumericPropertySpec(
139+
spec: AnnotationPropertySpec,
140+
): spec is AnnotationNumericPropertySpec {
141+
return spec.type !== "rgb" && spec.type !== "rgba";
142+
}
143+
144+
export function isAnnotationTagPropertySpec(
145+
spec: AnnotationPropertySpec,
146+
): spec is AnnotationTagPropertySpec {
147+
return spec.type === "uint8" && spec.tag !== undefined;
148+
}
149+
130150
export interface AnnotationPropertyTypeHandler {
131151
serializedBytes(rank: number): number;
132152
alignment(rank: number): number;
@@ -569,6 +589,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
569589
);
570590
let enumValues: number[] | undefined;
571591
let enumLabels: string[] | undefined;
592+
let tag: string | undefined;
572593
switch (type) {
573594
case "rgb":
574595
case "rgba":
@@ -593,6 +614,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
593614
),
594615
);
595616
}
617+
tag = verifyOptionalObjectProperty(obj, "tag", verifyString);
596618
}
597619
}
598620
return {
@@ -602,15 +624,23 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
602624
default: defaultValue,
603625
enumValues,
604626
enumLabels,
627+
tag,
605628
} as AnnotationPropertySpec;
606629
}
607630

608631
function annotationPropertySpecToJson(spec: AnnotationPropertySpec) {
609632
const defaultValue = spec.default;
633+
const isNumeric = isAnnotationNumericPropertySpec(spec);
634+
const tag = isNumeric ? spec.tag : undefined;
635+
const enum_values = isNumeric ? spec.enumValues : undefined;
636+
const enum_labels = isNumeric ? spec.enumLabels : undefined;
610637
return {
611638
id: spec.identifier,
612639
description: spec.description,
613640
type: spec.type,
641+
tag,
642+
enum_values,
643+
enum_labels,
614644
default:
615645
defaultValue === 0
616646
? undefined
@@ -1000,7 +1030,7 @@ export const annotationTypeHandlers: Record<
10001030
export interface AnnotationSchema {
10011031
rank: number;
10021032
relationships: readonly string[];
1003-
properties: readonly AnnotationPropertySpec[];
1033+
properties: WatchableValue<readonly Readonly<AnnotationPropertySpec>[]>;
10041034
}
10051035

10061036
export function annotationToJson(
@@ -1020,8 +1050,8 @@ export function annotationToJson(
10201050
segments.map((x) => x.toString()),
10211051
);
10221052
}
1023-
if (schema.properties.length !== 0) {
1024-
const propertySpecs = schema.properties;
1053+
const propertySpecs = schema.properties.value;
1054+
if (propertySpecs.length !== 0) {
10251055
result.props = annotation.properties.map((prop, i) =>
10261056
annotationPropertyTypeHandlers[propertySpecs[i].type].serializeJson(prop),
10271057
);
@@ -1061,9 +1091,9 @@ function restoreAnnotation(
10611091
);
10621092
});
10631093
const properties = verifyObjectProperty(obj, "props", (propsObj) => {
1064-
const propSpecs = schema.properties;
1094+
const propSpecs = schema.properties.value;
10651095
if (propsObj === undefined) return propSpecs.map((x) => x.default);
1066-
return parseArray(expectArray(propsObj, schema.properties.length), (x, i) =>
1096+
return parseArray(expectArray(propsObj, propSpecs.length), (x, i) =>
10671097
annotationPropertyTypeHandlers[propSpecs[i].type].deserializeJson(x),
10681098
);
10691099
});
@@ -1111,13 +1141,15 @@ export class AnnotationSource
11111141
constructor(
11121142
rank: number,
11131143
public readonly relationships: readonly string[] = [],
1114-
public readonly properties: Readonly<AnnotationPropertySpec>[] = [],
1144+
public readonly properties: WatchableValue<
1145+
readonly Readonly<AnnotationPropertySpec>[]
1146+
> = new WatchableValue([]),
11151147
) {
11161148
super();
11171149
this.rank_ = rank;
11181150
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
11191151
rank,
1120-
properties,
1152+
properties.value,
11211153
);
11221154
}
11231155

@@ -1261,16 +1293,56 @@ export class LocalAnnotationSource extends AnnotationSource {
12611293

12621294
constructor(
12631295
public watchableTransform: WatchableCoordinateSpaceTransform,
1264-
properties: AnnotationPropertySpec[],
1296+
public readonly properties: WatchableValue<
1297+
AnnotationPropertySpec[]
1298+
> = new WatchableValue([]),
12651299
relationships: string[],
12661300
) {
12671301
super(watchableTransform.value.sourceRank, relationships, properties);
12681302
this.curCoordinateTransform = watchableTransform.value;
12691303
this.registerDisposer(
12701304
watchableTransform.changed.add(() => this.ensureUpdated()),
12711305
);
1306+
1307+
this.registerDisposer(
1308+
properties.changed.add(() => {
1309+
this.updateAnnotationPropertySerializers();
1310+
this.changed.dispatch();
1311+
}),
1312+
);
1313+
}
1314+
1315+
updateAnnotationPropertySerializers() {
1316+
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
1317+
this.rank_,
1318+
this.properties.value,
1319+
);
12721320
}
12731321

1322+
addProperty(property: AnnotationPropertySpec) {
1323+
this.properties.value.push(property);
1324+
for (const annotation of this) {
1325+
annotation.properties.push(property.default);
1326+
}
1327+
this.properties.changed.dispatch();
1328+
}
1329+
1330+
removeProperty(identifier: string) {
1331+
const propertyIndex = this.properties.value.findIndex(
1332+
(x) => x.identifier === identifier,
1333+
);
1334+
this.properties.value.splice(propertyIndex, 1);
1335+
for (const annotation of this) {
1336+
annotation.properties.splice(propertyIndex, 1);
1337+
}
1338+
this.properties.changed.dispatch();
1339+
}
1340+
1341+
getTagProperties = () => {
1342+
const { properties } = this;
1343+
return properties.value.filter(isAnnotationTagPropertySpec);
1344+
};
1345+
12741346
ensureUpdated() {
12751347
const transform = this.watchableTransform.value;
12761348
const { curCoordinateTransform } = this;
@@ -1325,10 +1397,7 @@ export class LocalAnnotationSource extends AnnotationSource {
13251397
}
13261398
if (this.rank_ !== sourceRank) {
13271399
this.rank_ = sourceRank;
1328-
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
1329-
this.rank_,
1330-
this.properties,
1331-
);
1400+
this.updateAnnotationPropertySerializers();
13321401
}
13331402
this.changed.dispatch();
13341403
}

src/annotation/renderlayer.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,16 +474,18 @@ function AnnotationRenderLayer<
474474
private renderHelpers: AnnotationRenderHelper[] = [];
475475
private tempChunkPosition: Float32Array;
476476

477-
handleRankChanged() {
477+
handleRankChanged(force = false) {
478478
const { rank } = this.base.source;
479-
if (rank === this.curRank) return;
479+
if (!force && rank === this.curRank) return;
480480
this.curRank = rank;
481481
this.tempChunkPosition = new Float32Array(rank);
482482
const { renderHelpers, gl } = this;
483483
for (const oldHelper of renderHelpers) {
484484
oldHelper.dispose();
485485
}
486-
const { properties } = this.base.source;
486+
const {
487+
properties: { value: properties },
488+
} = this.base.source;
487489
const { displayState } = this.base.state;
488490
for (const annotationType of annotationTypes) {
489491
const handler = getAnnotationTypeRenderHandler(annotationType);
@@ -522,6 +524,12 @@ function AnnotationRenderLayer<
522524
});
523525
this.role = base.state.role;
524526
this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
527+
this.registerDisposer(
528+
base.source.properties.changed.add(() => {
529+
// todo, does it make sense to run this whole function? Or should we pass the watchable value to renderHelperConstructor?
530+
this.handleRankChanged(true);
531+
}),
532+
);
525533
this.handleRankChanged();
526534
this.registerDisposer(
527535
this.base.state.displayState.shaderControls.histogramSpecifications.producerVisibility.add(
@@ -780,7 +788,9 @@ function AnnotationRenderLayer<
780788
transformPickedValue(pickState: PickState) {
781789
const { pickedAnnotationBuffer } = pickState;
782790
if (pickedAnnotationBuffer === undefined) return undefined;
783-
const { properties } = this.base.source;
791+
const {
792+
properties: { value: properties },
793+
} = this.base.source;
784794
if (properties.length === 0) return undefined;
785795
const {
786796
pickedAnnotationBufferBaseOffset,

src/datasource/graphene/frontend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ function makeColoredAnnotationState(
763763
const { subsourceEntry } = loadedSubsource;
764764
const source = new LocalAnnotationSource(
765765
loadedSubsource.loadedDataSource.transform,
766-
[],
766+
new WatchableValue([]),
767767
["associated segments"],
768768
);
769769

0 commit comments

Comments
 (0)