From 58748f982e9b15b0076dddd5151017b30ebffb65 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Tue, 6 Dec 2022 16:51:02 -0500 Subject: [PATCH] initial find path tool --- config/custom-keybinds.json | 5 + .../datasource/graphene/frontend.ts | 457 +++++++++++++++--- .../datasource/graphene/graphene.css | 41 +- src/neuroglancer/ui/annotations.ts | 56 +++ src/neuroglancer/ui/tool.css | 1 + 5 files changed, 503 insertions(+), 57 deletions(-) diff --git a/config/custom-keybinds.json b/config/custom-keybinds.json index 2ce94dbaf..0a404e156 100644 --- a/config/custom-keybinds.json +++ b/config/custom-keybinds.json @@ -8,5 +8,10 @@ "layer": "segmentation", "tool": "grapheneMulticutSegments", "provider": "graphene" + }, + "keyp": { + "layer": "segmentation", + "tool": "grapheneFindPath", + "provider": "graphene" } } diff --git a/src/neuroglancer/datasource/graphene/frontend.ts b/src/neuroglancer/datasource/graphene/frontend.ts index da4c18482..cda0448bf 100644 --- a/src/neuroglancer/datasource/graphene/frontend.ts +++ b/src/neuroglancer/datasource/graphene/frontend.ts @@ -16,9 +16,9 @@ import './graphene.css'; -import {AnnotationReference, AnnotationType, Line, LocalAnnotationSource, makeDataBoundsBoundingBoxAnnotationSet, Point} from 'neuroglancer/annotation'; import {AnnotationDisplayState, AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state'; import {LayerChunkProgressInfo} from 'neuroglancer/chunk_manager/base'; +import {AnnotationReference, AnnotationSource, AnnotationType, Line, LocalAnnotationSource, makeDataBoundsBoundingBoxAnnotationSet, Point} from 'neuroglancer/annotation'; import {ChunkManager, WithParameters} from 'neuroglancer/chunk_manager/frontend'; import {makeIdentityTransform} from 'neuroglancer/coordinate_transform'; import {CredentialsManager} from 'neuroglancer/credentials_provider'; @@ -43,7 +43,7 @@ import {SliceViewPanelRenderLayer, SliceViewRenderLayer} from 'neuroglancer/slic import {StatusMessage} from 'neuroglancer/status'; import {TrackableBoolean, TrackableBooleanCheckbox} from 'neuroglancer/trackable_boolean'; import {makeCachedLazyDerivedWatchableValue, NestedStateManager, registerNested, TrackableValue, WatchableSet, WatchableValue, WatchableValueInterface} from 'neuroglancer/trackable_value'; -import {AnnotationLayerView, MergedAnnotationStates, PlaceLineTool} from 'neuroglancer/ui/annotations'; +import {AnnotationLayerView, MergedAnnotationStates, PlaceLineTool, makeAnnotationListElementTest} from 'neuroglancer/ui/annotations'; import {LayerTool, makeToolActivationStatusMessageWithHeader, makeToolButton, registerLegacyTool, registerTool, ToolActivation} from 'neuroglancer/ui/tool'; import {Uint64Set} from 'neuroglancer/uint64_set'; import {packColor} from 'neuroglancer/util/color'; @@ -61,6 +61,8 @@ import {Uint64} from 'neuroglancer/util/uint64'; import {makeDeleteButton} from 'neuroglancer/widget/delete_button'; import {DependentViewContext} from 'neuroglancer/widget/dependent_view_widget'; import {makeIcon} from 'neuroglancer/widget/icon'; +import {MultiscaleAnnotationSource} from 'src/neuroglancer/annotation/frontend_source'; +import {removeChildren} from 'src/neuroglancer/util/dom'; function vec4FromVec3(vec: vec3, alpha = 0) { const res = vec4.clone([...vec]); @@ -79,6 +81,7 @@ const RED_COLOR_SEGMENT_PACKED = new Uint64(packColor(RED_COLOR_SEGMENT)); const BLUE_COLOR_SEGMENT_PACKED = new Uint64(packColor(BLUE_COLOR_SEGMENT)); const TRANSPARENT_COLOR_PACKED = new Uint64(packColor(TRANSPARENT_COLOR)); const MULTICUT_OFF_COLOR = vec4.fromValues(0, 0, 0, 0.5); +const WHITE_COLOR = vec3.fromValues(1, 1, 1); class GrapheneMeshSource extends (WithParameters(WithCredentialsProvider()(MeshSource), MeshSourceParameters)) { @@ -459,11 +462,12 @@ function getGraphLoadedSubsource(layer: SegmentationUserLayer) { } function makeColoredAnnotationState( - layer: SegmentationUserLayer, loadedSubsource: LoadedDataSubsource, subsubsourceId: string, - color: vec3) { + layer: SegmentationUserLayer, loadedSubsource: LoadedDataSubsource, + subsubsourceId: string, color: vec3, readonly=false) { const {subsourceEntry} = loadedSubsource; const source = new LocalAnnotationSource(loadedSubsource.loadedDataSource.transform, [], ['associated segments']); - + source.readonly = readonly; + const displayState = new AnnotationDisplayState(); displayState.color.value.set(color); @@ -509,28 +513,47 @@ function restoreSegmentSelection(obj: any): SegmentSelection { } } +const segmentSelectionToJSON = (x: SegmentSelection) => { + return { + [SEGMENT_ID_JSON_KEY]: x.segmentId.toJSON(), + [ROOT_ID_JSON_KEY]: x.rootId.toJSON(), + [POSITION_JSON_KEY]: [...x.position], + } +} + const ID_JSON_KEY = 'id'; -const ERROR_JSON_KEY = 'error'; +const SEGMENT_ID_JSON_KEY = 'segmentId'; +const ROOT_ID_JSON_KEY = 'rootId'; +const POSITION_JSON_KEY = 'position'; +const SINK_JSON_KEY = 'sink'; +const SOURCE_JSON_KEY = 'source'; + const MULTICUT_JSON_KEY = 'multicut'; const FOCUS_SEGMENT_JSON_KEY = 'focusSegment'; const SINKS_JSON_KEY = 'sinks'; const SOURCES_JSON_KEY = 'sources'; -const SEGMENT_ID_JSON_KEY = 'segmentId'; -const ROOT_ID_JSON_KEY = 'rootId'; -const POSITION_JSON_KEY = 'position'; + const MERGE_JSON_KEY = 'merge'; const MERGES_JSON_KEY = 'merges'; const AUTOSUBMIT_JSON_KEY = 'autosubmit'; -const SINK_JSON_KEY = 'sink'; -const SOURCE_JSON_KEY = 'source'; -const MERGED_ROOT_JSON_KEY = 'mergedRoot'; const LOCKED_JSON_KEY = 'locked'; +const MERGED_ROOT_JSON_KEY = 'mergedRoot'; +const ERROR_JSON_KEY = 'error'; + +const FIND_PATH_JSON_KEY = 'findPath'; +const TARGET_JSON_KEY = "target"; +const CENTROIDS_JSON_KEY = "centroids"; +const PRECISION_MODE_JSON_KEY = "precision"; + + + class GrapheneState implements Trackable { changed = new NullarySignal(); public multicutState = new MulticutState(); public mergeState = new MergeState(); + public findPathState = new FindPathState(); constructor() { this.multicutState.changed.add(() => { @@ -539,17 +562,22 @@ class GrapheneState implements Trackable { this.mergeState.changed.add(() => { this.changed.dispatch(); }); + this.findPathState.changed.add(() => { + this.changed.dispatch() + }); } reset() { this.multicutState.reset(); this.mergeState.reset(); + this.findPathState.reset(); } toJSON() { return { [MULTICUT_JSON_KEY]: this.multicutState.toJSON(), [MERGE_JSON_KEY]: this.mergeState.toJSON(), + [FIND_PATH_JSON_KEY]: this.findPathState.toJSON(), } } @@ -560,6 +588,9 @@ class GrapheneState implements Trackable { verifyOptionalObjectProperty(x, MERGE_JSON_KEY, value => { this.mergeState.restoreState(value); }); + verifyOptionalObjectProperty(x, FIND_PATH_JSON_KEY, value => { + this.findPathState.restoreState(value); + }); } } @@ -589,14 +620,6 @@ class MergeState extends RefCounted implements Trackable { toJSON() { const {merges, autoSubmit} = this; - const segmentSelectionToJSON = (x: SegmentSelection) => { - return { - [SEGMENT_ID_JSON_KEY]: x.segmentId.toJSON(), - [ROOT_ID_JSON_KEY]: x.rootId.toJSON(), - [POSITION_JSON_KEY]: [...x.position], - } - } - const mergeToJSON = (x: MergeSubmission) => { const res: any = { [ID_JSON_KEY]: x.id, @@ -618,7 +641,7 @@ class MergeState extends RefCounted implements Trackable { return { [MERGES_JSON_KEY]: merges.value.filter(x=>x.source).map(mergeToJSON), [AUTOSUBMIT_JSON_KEY]: autoSubmit.toJSON(), - }; + } } restoreState(x: any) { @@ -650,6 +673,104 @@ class MergeState extends RefCounted implements Trackable { } } + +class FindPathState extends RefCounted implements Trackable { + changed = new NullarySignal(); + + source = new TrackableValue(undefined, x => x); + target = new TrackableValue(undefined, x => x); + centroids = new TrackableValue([], x => x); + precisionMode = new TrackableBoolean(true); + + get path() { + const path: Line[] = []; + const { + source: {value: source}, + target: {value: target}, + centroids: {value: centroids} + } = this; + if (!source || !target || centroids.length === 0) { + return path; + } + for (let i = 0; i < centroids.length - 1; i++) { + const pointA = centroids[i]; + const pointB = centroids[i+1]; + const line: Line = { + pointA: vec3.fromValues(pointA[0], pointA[1], pointA[2]), + pointB: vec3.fromValues(pointB[0], pointB[1], pointB[2]), + id: '', + type: AnnotationType.LINE, + properties: [], + }; + path.push(line); + } + const firstLine: Line = { + pointA: source.position, + pointB: path[0].pointA, + id: '', + type: AnnotationType.LINE, + properties: [], + }; + const lastLine: Line = { + pointA: path[path.length - 1].pointB, + pointB: target.position, + id: '', + type: AnnotationType.LINE, + properties: [], + }; + + return [firstLine, ...path, lastLine]; + } + + constructor() { + super(); + this.registerDisposer(this.source.changed.add(this.changed.dispatch)); + this.registerDisposer(this.target.changed.add(this.changed.dispatch)); + this.registerDisposer(this.centroids.changed.add(this.changed.dispatch)); + } + + reset() { + this.source.reset(); + this.target.reset(); + this.centroids.reset(); + this.precisionMode.reset(); + } + + toJSON() { + const { + source: {value: source}, + target: {value: target}, + centroids, + precisionMode, + } = this; + return { + [SOURCE_JSON_KEY]: source ? segmentSelectionToJSON(source) : undefined, + [TARGET_JSON_KEY]: target ? segmentSelectionToJSON(target) : undefined, + [CENTROIDS_JSON_KEY]: centroids.toJSON(), + [PRECISION_MODE_JSON_KEY]: precisionMode.toJSON(), + }; + } + + restoreState(x: any) { + verifyOptionalObjectProperty( + x, SOURCE_JSON_KEY, value => { + this.source.restoreState(restoreSegmentSelection(value)); + }); + verifyOptionalObjectProperty( + x, TARGET_JSON_KEY, value => { + this.target.restoreState(restoreSegmentSelection(value)); + }); + verifyOptionalObjectProperty( + x, CENTROIDS_JSON_KEY, value => { + this.centroids.restoreState(value); + }); + verifyOptionalObjectProperty( + x, PRECISION_MODE_JSON_KEY, value => { + this.precisionMode.restoreState(value); + }); + } +} + class MulticutState extends RefCounted implements Trackable { changed = new NullarySignal(); @@ -677,7 +798,7 @@ class MulticutState extends RefCounted implements Trackable { } reset() { - this.focusSegment.value = undefined; + this.focusSegment.reset(); this.blueGroup.value = false; this.sinks.clear(); this.sources.clear(); @@ -685,14 +806,6 @@ class MulticutState extends RefCounted implements Trackable { toJSON() { const {focusSegment, sinks, sources} = this; - - const segmentSelectionToJSON = (x: SegmentSelection) => { - return { - [SEGMENT_ID_JSON_KEY]: x.segmentId.toJSON(), [ROOT_ID_JSON_KEY]: x.rootId.toJSON(), - [POSITION_JSON_KEY]: [...x.position], - } - }; - return { [FOCUS_SEGMENT_JSON_KEY]: focusSegment.toJSON(), [SINKS_JSON_KEY]: [...sinks].map(segmentSelectionToJSON), @@ -750,6 +863,8 @@ class GraphConnection extends SegmentationGraphSourceConnection { public annotationLayerStates: AnnotationLayerState[] = []; public mergeAnnotationState: AnnotationLayerState; + public findPathAnnotationState: AnnotationLayerState; + constructor( public graph: GrapheneGraphSource, private layer: SegmentationUserLayer, private chunkSource: GrapheneMultiscaleVolumeChunkSource, public state: GrapheneState) { @@ -769,12 +884,12 @@ class GraphConnection extends SegmentationGraphSourceConnection { this.visibleSegmentsChanged(segmentIds, add); }); - const {annotationLayerStates, state: {multicutState}} = this; + const {annotationLayerStates, state: {multicutState, findPathState}} = this; const loadedSubsource = getGraphLoadedSubsource(layer)!; const redGroup = makeColoredAnnotationState(layer, loadedSubsource, 'sinks', RED_COLOR); const blueGroup = makeColoredAnnotationState(layer, loadedSubsource, 'sources', BLUE_COLOR); synchronizeAnnotationSource(multicutState.sinks, redGroup); - synchronizeAnnotationSource(multicutState.sources, blueGroup) + synchronizeAnnotationSource(multicutState.sources, blueGroup); annotationLayerStates.push(redGroup, blueGroup); if (layer.tool.value instanceof MergeSegmentsPlaceLineTool) { @@ -863,6 +978,38 @@ class GraphConnection extends SegmentationGraphSourceConnection { } }); } + + // const findPathPointsGroup = makeColoredAnnotationState(layer, loadedSubsource, "findpath", WHITE_COLOR); + // synchronizeAnnotationSource(findPathState.points, findPathPointsGroup); + + const clearAnnotations = (source: AnnotationSource|MultiscaleAnnotationSource) => { + for (const annotation of source) { + source.delete(source.getReference(annotation.id)); + } + }; + + const findPathGroup = makeColoredAnnotationState(layer, loadedSubsource, "findpath", WHITE_COLOR, false); + + this.findPathAnnotationState = findPathGroup; + + const findPathChanged = () => { + const {path, source, target} = findPathState; + const annotationSource = findPathGroup.source; + clearAnnotations(annotationSource); + if (source.value) { + addSelection(annotationSource, source.value, "find path source"); + } + if (target.value) { + addSelection(annotationSource, target.value, "find path target"); + } + + for (const line of path) { + line.id = ''; // TODO, is it a bug that this is necessary? annotationMap is empty if I step through it but logging shows it isn't empty + annotationSource.add(line); + } + }; + findPathState.changed.add(findPathChanged); + findPathChanged(); // initial state } createRenderLayers( @@ -971,6 +1118,39 @@ class GraphConnection extends SegmentationGraphSourceConnection { } } + async submitFindPath(precisionMode: boolean, annotationToNanometers: Float64Array): Promise { + const {state: {findPathState}} = this; + const {source, target} = findPathState; + if (!source.value || !target.value) return false; + const centroids = await this.graph.graphServer.findPath(source.value, target.value, precisionMode, annotationToNanometers); + StatusMessage.showTemporaryMessage('Path found!', 5000); + findPathState.centroids.value = centroids; + return true; + } + + /* + setPath(path: Line[]) { + if (this.ready()) { + this.annotationSource.clear(); + const firstLine: Line = + {pointA: this.source!.point, pointB: path[0].pointA, id: '', type: AnnotationType.LINE}; + this.annotationSource.add(firstLine); + for (const line of path) { + this.annotationSource.add(line); + } + const lastLine: Line = { + pointA: path[path.length - 1].pointB, + pointB: this.target!.point, + id: '', + type: AnnotationType.LINE + }; + this.annotationSource.add(lastLine); + this._hasPath = true; + this.changed.dispatch(); + } + } + */ + async submitMulticut(annotationToNanometers: Float64Array): Promise { const {state: {multicutState}} = this; const {sinks, sources} = multicutState; @@ -1147,15 +1327,13 @@ async function withErrorMessageHTTP(promise: Promise, options: { } catch (e) { if (e instanceof HttpError && e.response) { const {errorPrefix = ''} = options; - const msg = await parseGrapheneError(e); - if (msg) { - if (!status) { - status = new StatusMessage(true); - } - status.setErrorMessage(errorPrefix + msg); - status.setVisible(true); - throw new Error(`[${e.response.status}] ${errorPrefix}${msg}`); + const msg = await parseGrapheneError(e) || 'unknown error'; + if (!status) { + status = new StatusMessage(true); } + status.setErrorMessage(errorPrefix + msg); + status.setVisible(true); + throw new Error(`[${e.response.status}] ${errorPrefix}${msg}`); } throw e; } @@ -1280,6 +1458,43 @@ class GrapheneGraphServerInterface { } return res; } + + async findPath(first: SegmentSelection, second: SegmentSelection, precisionMode: boolean, annotationToNanometers: Float64Array): + Promise { + const {url} = this; + if (url === '') { + return Promise.reject(GRAPH_SERVER_NOT_SPECIFIED); + } + + const promise = + cancellableFetchSpecialOk(this.credentialsProvider, `${url}/graph/find_path?int64_as_str=1&precision_mode=${Number(precisionMode)}`, { + method: 'POST', + body: JSON.stringify([ + [String(first.rootId), ...first.position.map((val, i) => val * annotationToNanometers[i])], + [String(second.rootId), ...second.position.map((val, i) => val * annotationToNanometers[i])], + ]), + }, responseIdentity); + + const response = await withErrorMessageHTTP(promise, { + initialMessage: `Finding path between ${first.segmentId} and ${second.segmentId}`, + errorPrefix: 'Path finding failed: ' + }); + const jsonResponse = await response.json(); + const supervoxelCentroidsKey = 'centroids_list'; + const centroids = jsonResponse[supervoxelCentroidsKey]; + + const centroidsTransformed = centroids.map((point: number[]) => { + return point.map((val, i) => val / annotationToNanometers[i]); + }); + + const missingL2IdsKey = 'failed_l2_ids'; + const missingL2Ids = jsonResponse[missingL2IdsKey]; + if (missingL2Ids && missingL2Ids.length > 0) { + StatusMessage.showTemporaryMessage( + 'Some level 2 meshes are missing, so the path shown may have a poor level of detail.'); + } + return centroidsTransformed; + } } class GrapheneGraphSource extends SegmentationGraphSource { @@ -1330,6 +1545,9 @@ class GrapheneGraphSource extends SegmentationGraphSource { toolbox.appendChild(makeToolButton( context, layer.toolBinder, {toolJson: GRAPHENE_MERGE_SEGMENTS_TOOL_ID, label: 'Merge', title: 'Merge segments'})); + toolbox.appendChild(makeToolButton( + context, layer.toolBinder, + {toolJson: GRAPHENE_FIND_PATH_TOOL_ID, label: 'Find Path', title: 'Find Path'})); parent.appendChild(toolbox); parent.appendChild( context @@ -1473,6 +1691,7 @@ class SliceViewPanelChunkedGraphLayer extends SliceViewPanelRenderLayer { const GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID = 'grapheneMulticutSegments'; const GRAPHENE_MERGE_SEGMENTS_TOOL_ID = 'grapheneMergeSegments'; +const GRAPHENE_FIND_PATH_TOOL_ID = 'grapheneFindPath'; class MulticutAnnotationLayerView extends AnnotationLayerView { private _annotationStates: MergedAnnotationStates; @@ -1496,6 +1715,19 @@ class MulticutAnnotationLayerView extends AnnotationLayerView { } } +const addSelection = (source: AnnotationSource|MultiscaleAnnotationSource, selection: SegmentSelection, description?: string) => { + const annotation: Point = { + id: '', + point: selection.position, + type: AnnotationType.POINT, + properties: [], + relatedSegments: [[selection.segmentId, selection.rootId]], + description, + }; + const ref = source.add(annotation); + selection.annotationReference = ref; +} + const synchronizeAnnotationSource = (source: WatchableSet, state: AnnotationLayerState) => { const annotationSource = state.source; @@ -1504,30 +1736,16 @@ const synchronizeAnnotationSource = (source: WatchableSet, sta if (selection) source.delete(selection); }); - const addSelection = (selection: SegmentSelection) => { - const annotation: Point = { - id: '', - point: selection.position, - type: AnnotationType.POINT, - properties: [], - relatedSegments: [[selection.segmentId, selection.rootId]], - }; - const ref = annotationSource.add(annotation); - selection.annotationReference = ref; - } - source.changed.add((x, add) => { if (x === null) { for (const annotation of annotationSource) { - // using .clear does not remove annotations from the list - // (this.blueGroupAnnotationState.source as LocalAnnotationSource).clear(); annotationSource.delete(annotationSource.getReference(annotation.id)); } return; } if (add) { - addSelection(x); + addSelection(annotationSource, x); } else if (x.annotationReference) { annotationSource.delete(x.annotationReference); } @@ -1535,7 +1753,7 @@ const synchronizeAnnotationSource = (source: WatchableSet, sta // load initial state for (const selection of source) { - addSelection(selection); + addSelection(annotationSource, selection); } } @@ -1957,6 +2175,127 @@ class MergeSegmentsTool extends LayerTool { } } +const FIND_PATH_INPUT_EVENT_MAP = EventActionMap.fromObject({ + 'at:shift?+enter': {action: 'submit'}, + 'at:shift?+control+mousedown0': {action: 'add-point'}, +}); + +class FindPathTool extends LayerTool { + activate(activation: ToolActivation) { + const {layer} = this; + const {graphConnection: {value: graphConnection}} = layer; + if (!graphConnection || !(graphConnection instanceof GraphConnection)) return; + const {state: {findPathState}, findPathAnnotationState} = graphConnection; + const {source, target, precisionMode} = findPathState; + + // Ensure we use the same segmentationGroupState while activated. + const segmentationGroupState = this.layer.displayState.segmentationGroupState.value; + + const {body, header} = makeToolActivationStatusMessageWithHeader(activation); + header.textContent = 'Find Path'; + body.classList.add('graphene-find-path-status'); + + body.appendChild(makeIcon({ + text: 'Clear', + title: 'Clear Find Path', + onClick: () => { + findPathState.source.reset(); + findPathState.target.reset(); + findPathState.centroids.reset(); + }})); + + const submitAction = () => { + const loadedSubsource = getGraphLoadedSubsource(this.layer)!; + const annotationToNanometers = loadedSubsource.loadedDataSource.transform.inputSpace.value.scales.map(x => x / 1e-9); + graphConnection.submitFindPath(precisionMode.value, annotationToNanometers).then(success => { + success; + }); + } + + body.appendChild(makeIcon({ + text: 'Submit', + title: 'Submit Find Path', + onClick: () => { + submitAction(); + }})); + + const checkbox = activation.registerDisposer(new TrackableBooleanCheckbox(precisionMode)); + + const label = document.createElement('label'); + const labelText = document.createElement('span'); + labelText.textContent = "Precision mode: "; + label.appendChild(labelText); + label.title = 'todo'; + label.appendChild(checkbox.element); + body.appendChild(label); + + const points = document.createElement('div'); + points.style.display = 'contents'; + body.appendChild(points); + + const cancelBtn = makeIcon({ + text: 'Clear', + title: 'Clear selection', + onClick: () => { + while (points.firstChild) { + points.removeChild(points.firstChild); + } + body.removeChild(cancelBtn); + }}); + + const annotationElements = document.createElement('div'); + annotationElements.classList.add('graphene-find-path-status-annotations') + body.appendChild(annotationElements); + + const updateAnnotationElements = () => { + removeChildren(annotationElements); + for (const annotation of findPathAnnotationState.source) { + if (['find path source', 'find path target'].includes(annotation.description || '')) { + const annotationLabel = document.createElement('div'); + annotationLabel.innerHTML = annotation.description!; + annotationElements.appendChild(annotationLabel); + annotationElements.appendChild(makeAnnotationListElementTest(this.layer, annotation, findPathAnnotationState)); + } + } + }; + + findPathAnnotationState.source.changed.add(updateAnnotationElements); + updateAnnotationElements(); + + activation.bindInputEventMap(FIND_PATH_INPUT_EVENT_MAP); + + activation.bindAction('submit', event => { + event.stopPropagation(); + submitAction(); + }); + + activation.bindAction('add-point', event => { + event.stopPropagation(); + (async () => { + if (!source.value) { // first selection + const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments); + if (selection) { + source.value = selection; + } + } else if (!target.value) { + const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments); + if (selection) { + target.value = selection; + } + } + })() + }); + } + + toJSON() { + return GRAPHENE_FIND_PATH_TOOL_ID; + } + + get description() { + return `find path`; + } +} + registerTool(SegmentationUserLayer, GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID, layer => { return new MulticutSegmentsTool(layer, true); }); @@ -1965,8 +2304,14 @@ registerTool(SegmentationUserLayer, GRAPHENE_MERGE_SEGMENTS_TOOL_ID, layer => { return new MergeSegmentsTool(layer, true); }); +registerTool(SegmentationUserLayer, GRAPHENE_FIND_PATH_TOOL_ID, layer => { + return new FindPathTool(layer, true); +}); + const ANNOTATE_MERGE_LINE_TOOL_ID = 'annotateMergeLine'; registerLegacyTool( ANNOTATE_MERGE_LINE_TOOL_ID, (layer, options) => new MergeSegmentsPlaceLineTool(layer, options)); + + diff --git a/src/neuroglancer/datasource/graphene/graphene.css b/src/neuroglancer/datasource/graphene/graphene.css index 4c682eb96..2121e405a 100644 --- a/src/neuroglancer/datasource/graphene/graphene.css +++ b/src/neuroglancer/datasource/graphene/graphene.css @@ -50,7 +50,10 @@ gap: 10px; } -.graphene-merge-segments-status .neuroglancer-icon, .graphene-multicut-status .neuroglancer-icon { +.graphene-merge-segments-status .neuroglancer-icon, +.graphene-multicut-status .neuroglancer-icon, +.graphene-find-path-status .neuroglancer-icon + { height: 100%; } @@ -72,3 +75,39 @@ .graphene-merge-segments-point .neuroglancer-segment-list-entry-copy-container { display: none; } + +.graphene-find-path-status { + display: flex; + flex-direction: row; +} + +.graphene-find-path-status-annotations { + display: grid; + margin: 0 10px; + grid-template-columns: min-content min-content; + column-gap: 10px; + white-space: nowrap; +} + +.graphene-find-path-status-annotations:nth-child(2n-1) { + text-align: right; +} + +.graphene-find-path-status-annotations .neuroglancer-annotation-position > .neuroglancer-annotation-coordinate:nth-child(2) { + margin-left: 10px; + margin-right: 10px; +} + +.graphene-find-path-status > label { + display: flex; +} + +.graphene-find-path-status > label > span { + display: flex; + align-content: center; + flex-wrap: wrap; +} + +.graphene-find-path-status-annotations > .neuroglancer-annotation-list-entry { + background-color: black; +} diff --git a/src/neuroglancer/ui/annotations.ts b/src/neuroglancer/ui/annotations.ts index 16a947681..bb8748d6d 100644 --- a/src/neuroglancer/ui/annotations.ts +++ b/src/neuroglancer/ui/annotations.ts @@ -61,6 +61,62 @@ import {makeMoveToButton} from 'neuroglancer/widget/move_to_button'; import {Tab} from 'neuroglancer/widget/tab_view'; import {VirtualList, VirtualListSource} from 'neuroglancer/widget/virtual_list'; + export const makeAnnotationListElementTest = (layer: UserLayer, annotation: Annotation, state: AnnotationLayerState) => { + let gridTemplate = '[symbol] 2ch'; + + const chunkTransform = state.chunkTransform.value as ChunkTransformParameters; + const element = document.createElement('div'); + element.classList.add('neuroglancer-annotation-list-entry'); + element.style.gridAutoFlow = 'column'; + element.dataset.color = state.displayState.color.toString(); + element.style.gridTemplateColumns = gridTemplate; + const icon = document.createElement('div'); + icon.className = 'neuroglancer-annotation-icon'; + icon.textContent = annotationTypeHandlers[annotation.type].icon; + element.appendChild(icon); + + const globalDimensionIndices = [0,1,2]; + const localDimensionIndices: number[] = []; + + // let numRows = 0; + visitTransformedAnnotationGeometry(annotation, chunkTransform, (layerPosition, isVector) => { + isVector; + const position = document.createElement('div'); + position.className = 'neuroglancer-annotation-position'; + element.appendChild(position); + const addDims = + (viewDimensionIndices: readonly number[], layerDimensionIndices: readonly number[]) => { + for (const viewDim of viewDimensionIndices) { + const layerDim = layerDimensionIndices[viewDim]; + if (layerDim !== -1) { + const coord = Math.floor(layerPosition[layerDim]); + const coordElement = document.createElement('div'); + const text = coord.toString() + coordElement.textContent = text; + coordElement.classList.add('neuroglancer-annotation-coordinate'); + position.appendChild(coordElement); + } + } + }; + addDims( + globalDimensionIndices, chunkTransform.modelTransform.globalToRenderLayerDimensions); + addDims( + localDimensionIndices, chunkTransform.modelTransform.localToRenderLayerDimensions); + + if (!isVector) { + const moveButton = makeMoveToButton({ + title: 'Move to position', + onClick: () => { + setLayerPosition(layer, chunkTransform, layerPosition); + }, + }); + element.appendChild(moveButton); + } + }); + return element; + }; + + export class MergedAnnotationStates extends RefCounted implements WatchableValueInterface { changed = new NullarySignal(); diff --git a/src/neuroglancer/ui/tool.css b/src/neuroglancer/ui/tool.css index c9b472078..745b7f1f1 100644 --- a/src/neuroglancer/ui/tool.css +++ b/src/neuroglancer/ui/tool.css @@ -68,6 +68,7 @@ .neuroglancer-tool-status-header { align-self: center; + white-space: nowrap; } .neuroglancer-tool-status-body {