diff --git a/src/annotation/annotation_layer_state.ts b/src/annotation/annotation_layer_state.ts index d2c0aa56a..1fa7ab879 100644 --- a/src/annotation/annotation_layer_state.ts +++ b/src/annotation/annotation_layer_state.ts @@ -159,6 +159,7 @@ export class AnnotationDisplayState extends RefCounted { new WatchableAnnotationRelationshipStates(), ); ignoreNullSegmentFilter = new TrackableBoolean(true); + swapVisibleSegmentsOnMove = new TrackableBoolean(true); disablePicking = new WatchableValue(false); displayUnfiltered = makeCachedLazyDerivedWatchableValue( (map, ignoreNullSegmentFilter) => { diff --git a/src/annotation/frontend_source.ts b/src/annotation/frontend_source.ts index f150a4d12..f0c84629b 100644 --- a/src/annotation/frontend_source.ts +++ b/src/annotation/frontend_source.ts @@ -56,7 +56,7 @@ import { SliceViewChunkSource, } from "#src/sliceview/frontend.js"; import { StatusMessage } from "#src/status.js"; -import type { WatchableValue } from "#src/trackable_value.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"; @@ -532,7 +532,7 @@ export class MultiscaleAnnotationSource ) { super(); this.rank = options.rank; - this.properties.value = options.properties; + this.properties = new WatchableValue(options.properties); this.annotationPropertySerializers = makeAnnotationPropertySerializers( this.rank, this.properties.value, diff --git a/src/datasource/graphene/frontend.ts b/src/datasource/graphene/frontend.ts index 8b33d4acf..231f00856 100644 --- a/src/datasource/graphene/frontend.ts +++ b/src/datasource/graphene/frontend.ts @@ -1329,6 +1329,7 @@ export class GraphConnection extends SegmentationGraphSourceConnection { ) { super(graph, layer.displayState.segmentationGroupState.value); const segmentsState = layer.displayState.segmentationGroupState.value; + this.previousVisibleSegmentCount = segmentsState.visibleSegments.size; this.registerDisposer( segmentsState.selectedSegments.changed.add( (segmentIds: Uint64[] | Uint64 | null, add: boolean) => { @@ -1571,6 +1572,8 @@ export class GraphConnection extends SegmentationGraphSourceConnection { private lastDeselectionMessage: StatusMessage | undefined; private lastDeselectionMessageExists = false; + private previousVisibleSegmentCount: number; + private visibleSegmentsChanged(segments: Uint64[] | null, added: boolean) { const { segmentsState } = this; const { @@ -1597,16 +1600,18 @@ export class GraphConnection extends SegmentationGraphSourceConnection { } } if (segments === null) { - const leafSegmentCount = this.segmentsState.selectedSegments.size; this.segmentsState.segmentEquivalences.clear(); StatusMessage.showTemporaryMessage( - `Hid all ${leafSegmentCount} segments.`, + `Hid all ${this.previousVisibleSegmentCount} segment(s).`, 3000, ); return; } for (const segmentId of segments) { - if (!added) { + if ( + !added && + !isBaseSegmentId(segmentId, this.graph.info.graph.nBitsForLayerId) + ) { const segmentCount = [ ...segmentsState.segmentEquivalences.setElements(segmentId), ].length; // Approximation @@ -1616,7 +1621,7 @@ export class GraphConnection extends SegmentationGraphSourceConnection { this.lastDeselectionMessageExists = false; } this.lastDeselectionMessage = StatusMessage.showMessage( - `Hid ${segmentCount} segments.`, + `Hid ${segmentCount} segment(s).`, ); this.lastDeselectionMessageExists = true; setTimeout(() => { @@ -1627,6 +1632,7 @@ export class GraphConnection extends SegmentationGraphSourceConnection { }, 2000); } } + this.previousVisibleSegmentCount = segmentsState.visibleSegments.size; } private selectedSegmentsChanged(segments: Uint64[] | null, added: boolean) { @@ -1634,7 +1640,7 @@ export class GraphConnection extends SegmentationGraphSourceConnection { if (segments === null) { const leafSegmentCount = this.segmentsState.selectedSegments.size; StatusMessage.showTemporaryMessage( - `Deselected all ${leafSegmentCount} segments.`, + `Deselected all ${leafSegmentCount} segment(s).`, 3000, ); return; diff --git a/src/layer/annotation/index.ts b/src/layer/annotation/index.ts index 47164c3a4..d984e2859 100644 --- a/src/layer/annotation/index.ts +++ b/src/layer/annotation/index.ts @@ -51,6 +51,7 @@ import type { SegmentationDisplayState } from "#src/segmentation_display_state/f import type { TrackableBoolean } from "#src/trackable_boolean.js"; import { TrackableBooleanCheckbox } from "#src/trackable_boolean.js"; import { + ComputedWatchableValue, makeCachedLazyDerivedWatchableValue, WatchableValue, } from "#src/trackable_value.js"; @@ -159,6 +160,7 @@ interface LinkedSegmentationLayer { const LINKED_SEGMENTATION_LAYER_JSON_KEY = "linkedSegmentationLayer"; const FILTER_BY_SEGMENTATION_JSON_KEY = "filterBySegmentation"; const IGNORE_NULL_SEGMENT_FILTER_JSON_KEY = "ignoreNullSegmentFilter"; +const SWAP_VISIBLE_SEGMENTS_ON_MOVE_JSON_KEY = "swapVisbleSegmentsOnMove"; class LinkedSegmentationLayers extends RefCounted { changed = new NullarySignal(); @@ -705,7 +707,6 @@ export class AnnotationUserLayer extends Base { this.annotationProjectionRenderScaleTarget.changed.add( this.specificationChanged.dispatch, ); - this.registerDisposer( this.localAnnotationProperties.changed.add(() => { const { localAnnotations } = this; @@ -722,12 +723,18 @@ export class AnnotationUserLayer extends Base { order: -100, getter: () => new RenderingOptionsTab(this), }); - this.tabs.default = "annotations"; + const hideTagsTab = this.registerDisposer( + new ComputedWatchableValue(() => { + return this.localAnnotations === undefined; + }, this.dataSourcesChanged), + ); this.tabs.add("tags", { label: "Tags", order: 10, getter: () => new TagsTab(this), + hidden: hideTagsTab, }); + this.tabs.default = "annotations"; } syncTagTools = (tagIdentifiers: string[]) => { @@ -798,6 +805,9 @@ export class AnnotationUserLayer extends Base { this.annotationDisplayState.ignoreNullSegmentFilter.restoreState( specification[IGNORE_NULL_SEGMENT_FILTER_JSON_KEY], ); + this.annotationDisplayState.swapVisibleSegmentsOnMove.restoreState( + specification[SWAP_VISIBLE_SEGMENTS_ON_MOVE_JSON_KEY], + ); this.annotationDisplayState.shader.restoreState( specification[SHADER_JSON_KEY], ); @@ -1043,6 +1053,22 @@ export class AnnotationUserLayer extends Base { label.appendChild(checkbox.element); tab.element.appendChild(label); } + { + const checkbox = tab.registerDisposer( + new TrackableBooleanCheckbox( + this.annotationDisplayState.swapVisibleSegmentsOnMove, + ), + ); + const label = document.createElement("label"); + label.appendChild( + document.createTextNode( + "Swap visible segments when moving to annotation", + ), + ); + label.title = "Swap visible segments when moving to annotation"; + label.appendChild(checkbox.element); + tab.element.appendChild(label); + } tab.element.appendChild( tab.registerDisposer( new LinkedSegmentationLayersWidget(this.linkedSegmentationLayers), @@ -1072,6 +1098,8 @@ export class AnnotationUserLayer extends Base { : localAnnotationRelationships; x[IGNORE_NULL_SEGMENT_FILTER_JSON_KEY] = this.annotationDisplayState.ignoreNullSegmentFilter.toJSON(); + x[SWAP_VISIBLE_SEGMENTS_ON_MOVE_JSON_KEY] = + this.annotationDisplayState.swapVisibleSegmentsOnMove.toJSON(); x[SHADER_JSON_KEY] = this.annotationDisplayState.shader.toJSON(); x[SHADER_CONTROLS_JSON_KEY] = this.annotationDisplayState.shaderControls.toJSON(); diff --git a/src/ui/annotations.ts b/src/ui/annotations.ts index 1b3f45400..cb29916e5 100644 --- a/src/ui/annotations.ts +++ b/src/ui/annotations.ts @@ -172,6 +172,21 @@ export class MergedAnnotationStates } } +function getFirstVertexPosition(pos: Float32Array, annotation: Annotation) { + switch (annotation.type) { + case AnnotationType.AXIS_ALIGNED_BOUNDING_BOX: + case AnnotationType.LINE: + pos.set(annotation.pointA); + break; + case AnnotationType.POINT: + pos.set(annotation.point); + break; + case AnnotationType.ELLIPSOID: + pos.set(annotation.center); + break; + } +} + function getCenterPosition(center: Float32Array, annotation: Annotation) { switch (annotation.type) { case AnnotationType.AXIS_ALIGNED_BOUNDING_BOX: @@ -187,6 +202,7 @@ function getCenterPosition(center: Float32Array, annotation: Annotation) { break; } } +getCenterPosition; function setLayerPosition( layer: UserLayer, @@ -240,7 +256,7 @@ const moveToAnnotation = ( const { layerRank } = chunkTransform; const chunkPosition = new Float32Array(layerRank); const layerPosition = new Float32Array(layerRank); - getCenterPosition(chunkPosition, annotation); + getFirstVertexPosition(chunkPosition, annotation); matrix.transformPoint( layerPosition, chunkTransform.chunkToLayerTransform, @@ -249,6 +265,36 @@ const moveToAnnotation = ( layerRank, ); setLayerPosition(layer, chunkTransform, layerPosition); + if (state.displayState.swapVisibleSegmentsOnMove.value) { + showAnnotationSegments(annotation, state, true); + } +}; + +const showAnnotationSegments = ( + annotation: Annotation, + state: AnnotationLayerState, + onlyVisible = false, +) => { + const { relatedSegments } = annotation; + if (relatedSegments) { + const { source, displayState } = state; + const { relationships } = source; + const { relationshipStates } = displayState; + for (let i = 0, count = relationships.length; i < count; ++i) { + const segmentationState = relationshipStates.get(relationships[i]) + .segmentationState.value; + if (segmentationState) { + if (onlyVisible) { + segmentationState.segmentationGroupState.value.visibleSegments.clear(); + } + for (const segmentList of relatedSegments) { + segmentationState.segmentationGroupState.value.visibleSegments.add( + segmentList, + ); + } + } + } + } }; export class AnnotationLayerView extends Tab {