diff --git a/src/annotation/index.ts b/src/annotation/index.ts index db2dded24..fcd261743 100644 --- a/src/annotation/index.ts +++ b/src/annotation/index.ts @@ -54,7 +54,9 @@ import { import { parseDataTypeValue } from "#src/util/lerp.js"; import { getRandomHexString } from "#src/util/random.js"; import { NullarySignal, Signal } from "#src/util/signal.js"; +import { formatLength } from "#src/util/spatial_units.js"; import { Uint64 } from "#src/util/uint64.js"; +import { ALLOWED_UNITS } from "#src/widget/scale_bar.js"; export type AnnotationId = string; @@ -106,6 +108,7 @@ export interface AnnotationNumericPropertySpec min?: number; max?: number; step?: number; + format?: (x: number) => string; } export const propertyTypeDataType: Record< @@ -496,6 +499,9 @@ export function formatNumericProperty( property: AnnotationNumericPropertySpec, value: number, ): string { + if (property.format) { + return property.format(value); + } const formattedValue = property.type === "float32" ? value.toPrecision(6) : value.toString(); const { enumValues, enumLabels } = property; @@ -695,6 +701,15 @@ export interface AnnotationTypeHandler { annotation: T, callback: (vec: Float32Array, isVector: boolean) => void, ) => void; + defaultProperties: ( + annotation: T, + layerPosition: Float32Array[], + scales: Float64Array, + units: readonly string[], + ) => { + properties: AnnotationNumericPropertySpec[]; + values: number[]; + }; } function serializeFloatVector( @@ -751,6 +766,32 @@ function deserializeTwoFloatVectors( return offset; } +function lineLength( + annotationLayerPositions: Float32Array[], + scales: Float64Array, + units: readonly string[], +) { + if (annotationLayerPositions.length !== 2) { + return; + } + const [pointA, pointB] = annotationLayerPositions; + const scalesRank = scales.length; + const lineRank = pointA.length; + if (scalesRank < lineRank) { + return; + } + let lengthSquared = 0; + for (let dim = 0; dim < lineRank; dim++) { + const unitInfo = ALLOWED_UNITS.find((x) => x.unit === units[dim]); + if (!unitInfo) { + return; + } + const voxelToNanometers = scales[dim] * unitInfo.lengthInNanometers; + lengthSquared += ((pointA[dim] - pointB[dim]) * voxelToNanometers) ** 2; + } + return Math.sqrt(lengthSquared); +} + export const annotationTypeHandlers: Record< AnnotationType, AnnotationTypeHandler @@ -814,6 +855,28 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, + defaultProperties( + annotation: Line, + annotationLayerPositions: Float32Array[], + scales: Float64Array, + units: readonly string[], + ) { + annotation; + const properties: AnnotationNumericPropertySpec[] = []; + const values: number[] = []; + const length = lineLength(annotationLayerPositions, scales, units); + if (length) { + properties.push({ + type: "float32", + identifier: "Length", + default: 0, + description: "Length of the line annotation in nanometers", + format: formatLength, + }); + values.push(length); + } + return { properties, values }; + }, }, [AnnotationType.POINT]: { icon: "⚬", @@ -858,6 +921,18 @@ export const annotationTypeHandlers: Record< visitGeometry(annotation: Point, callback) { callback(annotation.point, false); }, + defaultProperties( + annotation: Point, + layerPosition: Float32Array[], + scales: Float64Array, + units: string[], + ) { + annotation; + layerPosition; + scales; + units; + return { properties: [], values: [] }; + }, }, [AnnotationType.AXIS_ALIGNED_BOUNDING_BOX]: { icon: "❑", @@ -926,6 +1001,18 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, + defaultProperties( + annotation: AxisAlignedBoundingBox, + layerPosition: Float32Array[], + scales: Float64Array, + units: string[], + ) { + annotation; + layerPosition; + scales; + units; + return { properties: [], values: [] }; + }, }, [AnnotationType.ELLIPSOID]: { icon: "◎", @@ -994,6 +1081,18 @@ export const annotationTypeHandlers: Record< callback(annotation.center, false); callback(annotation.radii, true); }, + defaultProperties( + annotation: Ellipsoid, + layerPosition: Float32Array[], + scales: Float64Array, + units: string[], + ) { + annotation; + layerPosition; + scales; + units; + return { properties: [], values: [] }; + }, }, }; diff --git a/src/ui/annotations.ts b/src/ui/annotations.ts index 051b9a4e1..49b1e63e9 100644 --- a/src/ui/annotations.ts +++ b/src/ui/annotations.ts @@ -1783,6 +1783,8 @@ export function UserLayerWithAnnotationsMixin< icon.textContent = handler.icon; positionGrid.appendChild(icon); + const annotationLayerPositions: Float32Array[] = + []; if (layerRank !== 0) { const { layerDimensionNames } = ( chunkTransform as ChunkTransformParameters @@ -1800,6 +1802,7 @@ export function UserLayerWithAnnotationsMixin< annotation, chunkTransform as ChunkTransformParameters, (layerPosition, isVector) => { + annotationLayerPositions.push(layerPosition); const copyButton = makeCopyButton({ title: "Copy position", onClick: () => { @@ -1854,6 +1857,24 @@ export function UserLayerWithAnnotationsMixin< const { relationships, properties } = annotationLayer.source; const sourceReadonly = annotationLayer.source.readonly; + const globalCoordinateSpace = + this.manager.root.coordinateSpace.value; + const defaultProperties = annotationTypeHandlers[ + annotation.type + ].defaultProperties( + annotation, + annotationLayerPositions, + globalCoordinateSpace.scales, + globalCoordinateSpace.units, + ); + const allProperties = [ + ...defaultProperties.properties, + ...properties, + ]; + const allValues = [ + ...defaultProperties.values, + ...annotation.properties, + ]; // Add the ID to the annotation details. const label = document.createElement("label"); @@ -1872,8 +1893,10 @@ export function UserLayerWithAnnotationsMixin< label.appendChild(valueElement); parent.appendChild(label); - for (let i = 0, count = properties.length; i < count; ++i) { - const property = properties[i]; + for (let i = 0, count = allProperties.length; i < count; ++i) { + const property = allProperties[i]; + const value = allValues[i]; + const label = document.createElement("label"); label.classList.add("neuroglancer-annotation-property"); const idElement = document.createElement("span"); @@ -1886,7 +1909,6 @@ export function UserLayerWithAnnotationsMixin< if (description !== undefined) { label.title = description; } - const value = annotation.properties[i]; const valueElement = document.createElement("span"); valueElement.classList.add( "neuroglancer-annotation-property-value",