From 47f8d34e477eb2b3a8fd41465284b3f5d1db3581 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Sat, 11 Jan 2025 11:05:21 -0500 Subject: [PATCH 1/5] feat(annotation) Add default annontation properties add line length property --- src/annotation/index.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/ui/annotations.ts | 23 ++++++++++++++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/annotation/index.ts b/src/annotation/index.ts index db2dded24..1e7788a42 100644 --- a/src/annotation/index.ts +++ b/src/annotation/index.ts @@ -18,6 +18,7 @@ * @file Basic annotation data structures. */ +import { vec3 } from "gl-matrix"; import type { BoundingBox, CoordinateSpaceTransform, @@ -695,6 +696,13 @@ export interface AnnotationTypeHandler { annotation: T, callback: (vec: Float32Array, isVector: boolean) => void, ) => void; + defaultProperties: ( + annotation: T, + scales: vec3, + ) => { + properties: AnnotationNumericPropertySpec[]; + values: number[]; + }; } function serializeFloatVector( @@ -814,6 +822,24 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, + defaultProperties(annotation: Line, scales: vec3) { + return { + properties: [ + { + type: "float32", + identifier: "Length", + default: 0, + description: "Length of the line annotation in nanometers", + }, + ], + values: [ + vec3.dist( + vec3.mul(vec3.create(), scales, annotation.pointA as vec3), + vec3.mul(vec3.create(), scales, annotation.pointB as vec3), + ), + ], + }; + }, }, [AnnotationType.POINT]: { icon: "⚬", @@ -858,6 +884,11 @@ export const annotationTypeHandlers: Record< visitGeometry(annotation: Point, callback) { callback(annotation.point, false); }, + defaultProperties(annotation: Point, scales: vec3) { + annotation; + scales; + return { properties: [], values: [] }; + }, }, [AnnotationType.AXIS_ALIGNED_BOUNDING_BOX]: { icon: "❑", @@ -926,6 +957,11 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, + defaultProperties(annotation: AxisAlignedBoundingBox, scales: vec3) { + annotation; + scales; + return { properties: [], values: [] }; + }, }, [AnnotationType.ELLIPSOID]: { icon: "◎", @@ -994,6 +1030,11 @@ export const annotationTypeHandlers: Record< callback(annotation.center, false); callback(annotation.radii, true); }, + defaultProperties(annotation: Ellipsoid, scales: vec3) { + annotation; + scales; + return { properties: [], values: [] }; + }, }, }; diff --git a/src/ui/annotations.ts b/src/ui/annotations.ts index 051b9a4e1..c1592a734 100644 --- a/src/ui/annotations.ts +++ b/src/ui/annotations.ts @@ -1854,6 +1854,22 @@ export function UserLayerWithAnnotationsMixin< const { relationships, properties } = annotationLayer.source; const sourceReadonly = annotationLayer.source.readonly; + const globalCoordinateSpace = + this.manager.root.coordinateSpace.value; + const scales = new Float32Array( + globalCoordinateSpace.scales.map((x) => x / 1e-9), + ) as vec3; + const defaultProperties = annotationTypeHandlers[ + annotation.type + ].defaultProperties(annotation, scales); + 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 +1888,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 +1904,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", From 356c42c43a5d735a68a39bb8786afe370491b5c7 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Wed, 15 Jan 2025 22:25:22 -0500 Subject: [PATCH 2/5] added optional format override property to AnnotationNumericPropertySpec --- src/annotation/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/annotation/index.ts b/src/annotation/index.ts index 1e7788a42..bfbffd3d8 100644 --- a/src/annotation/index.ts +++ b/src/annotation/index.ts @@ -55,6 +55,7 @@ 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"; export type AnnotationId = string; @@ -107,6 +108,7 @@ export interface AnnotationNumericPropertySpec min?: number; max?: number; step?: number; + format?: (x: number) => string; } export const propertyTypeDataType: Record< @@ -497,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; @@ -830,6 +835,7 @@ export const annotationTypeHandlers: Record< identifier: "Length", default: 0, description: "Length of the line annotation in nanometers", + format: formatLength, }, ], values: [ From 03eaf932d74ec4cc0ffe2f55123ca8721062cadd Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Thu, 16 Jan 2025 10:27:18 -0500 Subject: [PATCH 3/5] no longer assume three dimensions when calculating default annotation properties --- src/annotation/index.ts | 56 ++++++++++++++++++++++++----------------- src/ui/annotations.ts | 5 +--- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/annotation/index.ts b/src/annotation/index.ts index bfbffd3d8..c39773080 100644 --- a/src/annotation/index.ts +++ b/src/annotation/index.ts @@ -18,7 +18,6 @@ * @file Basic annotation data structures. */ -import { vec3 } from "gl-matrix"; import type { BoundingBox, CoordinateSpaceTransform, @@ -703,7 +702,7 @@ export interface AnnotationTypeHandler { ) => void; defaultProperties: ( annotation: T, - scales: vec3, + scales: Float64Array, ) => { properties: AnnotationNumericPropertySpec[]; values: number[]; @@ -827,24 +826,32 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, - defaultProperties(annotation: Line, scales: vec3) { - return { - properties: [ - { - type: "float32", - identifier: "Length", - default: 0, - description: "Length of the line annotation in nanometers", - format: formatLength, - }, - ], - values: [ - vec3.dist( - vec3.mul(vec3.create(), scales, annotation.pointA as vec3), - vec3.mul(vec3.create(), scales, annotation.pointB as vec3), - ), - ], - }; + defaultProperties(annotation: Line, scales: Float64Array) { + const properties: AnnotationNumericPropertySpec[] = []; + const values: number[] = []; + const rank = scales.length; + if ( + rank === annotation.pointA.length && + rank === annotation.pointB.length + ) { + properties.push({ + type: "float32", + identifier: "Length", + default: 0, + description: "Length of the line annotation in nanometers", + format: formatLength, + }); + let length = 0; + for (let dim = 0; dim < rank; dim++) { + length += + (((annotation.pointA[dim] - annotation.pointB[dim]) / 1e-9) * + scales[dim]) ** + 2; + } + length = Math.sqrt(length); + values.push(length); + } + return { properties, values }; }, }, [AnnotationType.POINT]: { @@ -890,7 +897,7 @@ export const annotationTypeHandlers: Record< visitGeometry(annotation: Point, callback) { callback(annotation.point, false); }, - defaultProperties(annotation: Point, scales: vec3) { + defaultProperties(annotation: Point, scales: Float64Array) { annotation; scales; return { properties: [], values: [] }; @@ -963,7 +970,10 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, - defaultProperties(annotation: AxisAlignedBoundingBox, scales: vec3) { + defaultProperties( + annotation: AxisAlignedBoundingBox, + scales: Float64Array, + ) { annotation; scales; return { properties: [], values: [] }; @@ -1036,7 +1046,7 @@ export const annotationTypeHandlers: Record< callback(annotation.center, false); callback(annotation.radii, true); }, - defaultProperties(annotation: Ellipsoid, scales: vec3) { + defaultProperties(annotation: Ellipsoid, scales: Float64Array) { annotation; scales; return { properties: [], values: [] }; diff --git a/src/ui/annotations.ts b/src/ui/annotations.ts index c1592a734..ff9a56335 100644 --- a/src/ui/annotations.ts +++ b/src/ui/annotations.ts @@ -1856,12 +1856,9 @@ export function UserLayerWithAnnotationsMixin< const sourceReadonly = annotationLayer.source.readonly; const globalCoordinateSpace = this.manager.root.coordinateSpace.value; - const scales = new Float32Array( - globalCoordinateSpace.scales.map((x) => x / 1e-9), - ) as vec3; const defaultProperties = annotationTypeHandlers[ annotation.type - ].defaultProperties(annotation, scales); + ].defaultProperties(annotation, globalCoordinateSpace.scales); const allProperties = [ ...defaultProperties.properties, ...properties, From 324342f4085e6499466e89c0e4ba68025f9f6470 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Fri, 17 Jan 2025 15:59:13 -0500 Subject: [PATCH 4/5] use globalCoordinateSpace.units in defaultProperties rather than assuming meters split line length code into a separate function --- src/annotation/index.ts | 62 ++++++++++++++++++++++++++++++----------- src/ui/annotations.ts | 6 +++- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/annotation/index.ts b/src/annotation/index.ts index c39773080..50614d6c8 100644 --- a/src/annotation/index.ts +++ b/src/annotation/index.ts @@ -56,6 +56,7 @@ 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; @@ -703,6 +704,7 @@ export interface AnnotationTypeHandler { defaultProperties: ( annotation: T, scales: Float64Array, + units: readonly string[], ) => { properties: AnnotationNumericPropertySpec[]; values: number[]; @@ -763,6 +765,29 @@ function deserializeTwoFloatVectors( return offset; } +function lineLength( + line: Line, + scales: Float64Array, + units: readonly string[], +) { + const scalesRank = scales.length; + const lineRank = line.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 += + ((line.pointA[dim] - line.pointB[dim]) * voxelToNanometers) ** 2; + } + return Math.sqrt(lengthSquared); +} + export const annotationTypeHandlers: Record< AnnotationType, AnnotationTypeHandler @@ -826,14 +851,15 @@ export const annotationTypeHandlers: Record< callback(annotation.pointA, false); callback(annotation.pointB, false); }, - defaultProperties(annotation: Line, scales: Float64Array) { + defaultProperties( + annotation: Line, + scales: Float64Array, + units: readonly string[], + ) { const properties: AnnotationNumericPropertySpec[] = []; const values: number[] = []; - const rank = scales.length; - if ( - rank === annotation.pointA.length && - rank === annotation.pointB.length - ) { + const length = lineLength(annotation, scales, units); + if (length) { properties.push({ type: "float32", identifier: "Length", @@ -841,14 +867,6 @@ export const annotationTypeHandlers: Record< description: "Length of the line annotation in nanometers", format: formatLength, }); - let length = 0; - for (let dim = 0; dim < rank; dim++) { - length += - (((annotation.pointA[dim] - annotation.pointB[dim]) / 1e-9) * - scales[dim]) ** - 2; - } - length = Math.sqrt(length); values.push(length); } return { properties, values }; @@ -897,9 +915,14 @@ export const annotationTypeHandlers: Record< visitGeometry(annotation: Point, callback) { callback(annotation.point, false); }, - defaultProperties(annotation: Point, scales: Float64Array) { + defaultProperties( + annotation: Point, + scales: Float64Array, + units: string[], + ) { annotation; scales; + units; return { properties: [], values: [] }; }, }, @@ -973,9 +996,11 @@ export const annotationTypeHandlers: Record< defaultProperties( annotation: AxisAlignedBoundingBox, scales: Float64Array, + units: string[], ) { annotation; scales; + units; return { properties: [], values: [] }; }, }, @@ -1046,9 +1071,14 @@ export const annotationTypeHandlers: Record< callback(annotation.center, false); callback(annotation.radii, true); }, - defaultProperties(annotation: Ellipsoid, scales: Float64Array) { + defaultProperties( + annotation: Ellipsoid, + scales: Float64Array, + units: string[], + ) { annotation; scales; + units; return { properties: [], values: [] }; }, }, diff --git a/src/ui/annotations.ts b/src/ui/annotations.ts index ff9a56335..b78ca632a 100644 --- a/src/ui/annotations.ts +++ b/src/ui/annotations.ts @@ -1858,7 +1858,11 @@ export function UserLayerWithAnnotationsMixin< this.manager.root.coordinateSpace.value; const defaultProperties = annotationTypeHandlers[ annotation.type - ].defaultProperties(annotation, globalCoordinateSpace.scales); + ].defaultProperties( + annotation, + globalCoordinateSpace.scales, + globalCoordinateSpace.units, + ); const allProperties = [ ...defaultProperties.properties, ...properties, From 43c3e4aa0325ace712444b7a503ebb86aa33ed53 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Tue, 21 Jan 2025 15:55:52 -0500 Subject: [PATCH 5/5] fix default annotation properties to use transformed coordinates --- src/annotation/index.ts | 22 +++++++++++++++++----- src/ui/annotations.ts | 4 ++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/annotation/index.ts b/src/annotation/index.ts index 50614d6c8..fcd261743 100644 --- a/src/annotation/index.ts +++ b/src/annotation/index.ts @@ -703,6 +703,7 @@ export interface AnnotationTypeHandler { ) => void; defaultProperties: ( annotation: T, + layerPosition: Float32Array[], scales: Float64Array, units: readonly string[], ) => { @@ -766,12 +767,16 @@ function deserializeTwoFloatVectors( } function lineLength( - line: Line, + annotationLayerPositions: Float32Array[], scales: Float64Array, units: readonly string[], ) { + if (annotationLayerPositions.length !== 2) { + return; + } + const [pointA, pointB] = annotationLayerPositions; const scalesRank = scales.length; - const lineRank = line.pointA.length; + const lineRank = pointA.length; if (scalesRank < lineRank) { return; } @@ -782,8 +787,7 @@ function lineLength( return; } const voxelToNanometers = scales[dim] * unitInfo.lengthInNanometers; - lengthSquared += - ((line.pointA[dim] - line.pointB[dim]) * voxelToNanometers) ** 2; + lengthSquared += ((pointA[dim] - pointB[dim]) * voxelToNanometers) ** 2; } return Math.sqrt(lengthSquared); } @@ -853,12 +857,14 @@ export const annotationTypeHandlers: Record< }, defaultProperties( annotation: Line, + annotationLayerPositions: Float32Array[], scales: Float64Array, units: readonly string[], ) { + annotation; const properties: AnnotationNumericPropertySpec[] = []; const values: number[] = []; - const length = lineLength(annotation, scales, units); + const length = lineLength(annotationLayerPositions, scales, units); if (length) { properties.push({ type: "float32", @@ -917,10 +923,12 @@ export const annotationTypeHandlers: Record< }, defaultProperties( annotation: Point, + layerPosition: Float32Array[], scales: Float64Array, units: string[], ) { annotation; + layerPosition; scales; units; return { properties: [], values: [] }; @@ -995,10 +1003,12 @@ export const annotationTypeHandlers: Record< }, defaultProperties( annotation: AxisAlignedBoundingBox, + layerPosition: Float32Array[], scales: Float64Array, units: string[], ) { annotation; + layerPosition; scales; units; return { properties: [], values: [] }; @@ -1073,10 +1083,12 @@ export const annotationTypeHandlers: Record< }, 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 b78ca632a..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: () => { @@ -1860,6 +1863,7 @@ export function UserLayerWithAnnotationsMixin< annotation.type ].defaultProperties( annotation, + annotationLayerPositions, globalCoordinateSpace.scales, globalCoordinateSpace.units, );