Skip to content
Closed
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 @@ -55,6 +55,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 @@ -514,7 +515,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
new AnnotationMetadataChunkSource(this.chunkManager, this),
);
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
188 changes: 173 additions & 15 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 @@ -108,10 +109,16 @@ export interface AnnotationNumericPropertySpec
step?: number;
}

export function isAnnotationTypeNumeric(
type: AnnotationPropertySpec["type"],
): boolean {
return type !== "rgb" && type !== "rgba";
}

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

export const propertyTypeDataType: Record<
Expand Down Expand Up @@ -558,6 +565,36 @@ export function ensureUniqueAnnotationPropertyIds(
}
}

export function compareAnnotationSpecProperties(
a: Readonly<AnnotationPropertySpec>,
b: Readonly<AnnotationPropertySpec>,
) {
const bothNumeric =
isAnnotationNumericPropertySpec(a) && isAnnotationNumericPropertySpec(b);
const bothColor =
!isAnnotationNumericPropertySpec(a) && !isAnnotationNumericPropertySpec(b);
const sameValues = {
type: a.type === b.type,
identifier: a.identifier === b.identifier,
description: a.description === b.description,
default: a.default === b.default,
enumValues:
bothColor ||
(bothNumeric && arraysEqual(a.enumValues || [], b.enumValues || [])),
enumLabels:
bothColor ||
(bothNumeric && arraysEqual(a.enumLabels || [], b.enumLabels || [])),
enumLength:
bothColor ||
(bothNumeric &&
a.enumValues?.length === b.enumValues?.length &&
a.enumLabels?.length === b.enumLabels?.length),
};
// Same if all of the above are true.
const same = Object.values(sameValues).every((x) => x);
return { same, sameValues };
}

function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
verifyObject(obj);
const identifier = verifyObjectProperty(obj, "id", parseAnnotationPropertyId);
Expand Down Expand Up @@ -611,7 +648,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
} as AnnotationPropertySpec;
}

function annotationPropertySpecToJson(spec: AnnotationPropertySpec) {
function annotationPropertySpecToJson(spec: Readonly<AnnotationPropertySpec>) {
const defaultValue = spec.default;
const handler = annotationPropertyTypeHandlers[spec.type];
const isNumeric = isAnnotationNumericPropertySpec(spec);
Expand All @@ -632,7 +669,7 @@ function annotationPropertySpecToJson(spec: AnnotationPropertySpec) {
}

export function annotationPropertySpecsToJson(
specs: AnnotationPropertySpec[] | undefined,
specs: readonly Readonly<AnnotationPropertySpec>[] | undefined,
) {
if (specs === undefined || specs.length === 0) return undefined;
return specs.map(annotationPropertySpecToJson);
Expand Down Expand Up @@ -1013,7 +1050,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 @@ -1033,8 +1070,8 @@ export function annotationToJson(
Array.from(segments, (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 @@ -1083,9 +1120,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 @@ -1133,13 +1170,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 @@ -1273,6 +1312,30 @@ export class AnnotationSource
}
}

export function canConvertTypes(
oldType: AnnotationPropertySpec["type"],
newType: AnnotationPropertySpec["type"],
): boolean {
if (oldType === newType) return true;
const isOldTypeNumeric = isAnnotationTypeNumeric(oldType);
const isNewTypeNumeric = isAnnotationTypeNumeric(newType);
if (isOldTypeNumeric !== isNewTypeNumeric) return false;
if (oldType === "rgb" && newType === "rgba") return true;
if (newType === "float32") return true;

// Can convert between uint or int if newType is higher precision.
const sameFamily =
(oldType.startsWith("uint") && newType.startsWith("uint")) ||
(oldType.startsWith("int") && newType.startsWith("int"));

if (sameFamily) {
const oldBits = parseInt(oldType.replace(/\D/g, ""), 10);
const newBits = parseInt(newType.replace(/\D/g, ""), 10);
return oldBits < newBits;
}
return false;
}

export class LocalAnnotationSource extends AnnotationSource {
private curCoordinateTransform: CoordinateSpaceTransform;

Expand All @@ -1283,14 +1346,112 @@ 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) {
const { identifier } = property;
const properties = this.properties.value;
if (properties.some((p) => p.identifier === identifier)) {
console.error(`Property ${identifier} already exists`);
return;
}
properties.push(property);
for (const annotation of this) {
annotation.properties.push(property.default);
}
this.properties.changed.dispatch();
}

removeAllProperties() {
this.properties.value = [];
for (const annotation of this) {
annotation.properties = [];
}
this.properties.changed.dispatch();
}

removeProperty(identifier: string) {
const propertyIndex = this.properties.value.findIndex(
(x) => x.identifier === identifier,
);
if (propertyIndex === -1) {
console.error(`Property ${identifier} does not exist`);
return;
}
this.properties.value.splice(propertyIndex, 1);
for (const annotation of this) {
annotation.properties.splice(propertyIndex, 1);
}
this.properties.changed.dispatch();
}

updateProperty(
oldProperty: AnnotationPropertySpec,
newPropertyValues: Partial<AnnotationPropertySpec>,
) {
const newProperty = { ...oldProperty, ...newPropertyValues };
const { type: oldType } = oldProperty;
const { type: newType } = newProperty;
const isConvertible = canConvertTypes(oldType, newType);
if (!isConvertible) {
console.error(
`Cannot convert property ${oldProperty.identifier} from ${oldProperty.type} to ${newProperty.type}`,
);
return;
}

const convertValue = (value: any) => {
if (oldType === "rgb" && newType === "rgba") {
const rgba = new Uint8Array(4);
rgba[0] = value[0];
rgba[1] = value[1];
rgba[2] = value[2];
rgba[3] = 255;
return rgba;
}
return value;
};

const { identifier } = oldProperty;
const properties = this.properties.value;
const propertyIndex = properties.findIndex(
(x) => x.identifier === identifier,
);
if (propertyIndex === -1) {
console.error(`Property ${identifier} does not exist`);
return;
}
properties[propertyIndex] = newProperty;
for (const annotation of this) {
annotation.properties[propertyIndex] = convertValue(
annotation.properties[propertyIndex],
);
}
this.properties.changed.dispatch();
}

ensureUpdated() {
Expand Down Expand Up @@ -1347,10 +1508,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
19 changes: 15 additions & 4 deletions src/annotation/renderlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,16 +476,22 @@ function AnnotationRenderLayer<
private renderHelpers: AnnotationRenderHelper[] = [];
private tempChunkPosition: Float32Array;

handleRankChanged() {
handlePropertiesChanged = () => {
this.handleRankChanged(true /* forceUpdate */);
};

handleRankChanged(forceUpdate = false) {
const { rank } = this.base.source;
if (rank === this.curRank) return;
if (!forceUpdate && 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 @@ -524,6 +530,9 @@ function AnnotationRenderLayer<
});
this.role = base.state.role;
this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
this.registerDisposer(
base.source.properties.changed.add(this.handlePropertiesChanged),
);
this.handleRankChanged();
this.registerDisposer(
this.base.state.displayState.shaderControls.histogramSpecifications.producerVisibility.add(
Expand Down Expand Up @@ -781,7 +790,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 @@ -759,7 +759,7 @@ function makeColoredAnnotationState(
const { subsourceEntry } = loadedSubsource;
const source = new LocalAnnotationSource(
loadedSubsource.loadedDataSource.transform,
[],
new WatchableValue([]),
["associated segments"],
);

Expand Down
Loading
Loading