diff --git a/src/neuroglancer/datasource/graphene/backend.ts b/src/neuroglancer/datasource/graphene/backend.ts index f1f84fa704..d9d1e6c8e6 100644 --- a/src/neuroglancer/datasource/graphene/backend.ts +++ b/src/neuroglancer/datasource/graphene/backend.ts @@ -17,9 +17,9 @@ import {WithParameters} from 'neuroglancer/chunk_manager/backend'; import {WithSharedCredentialsProviderCounterpart} from 'neuroglancer/credentials_provider/shared_counterpart'; import {assignMeshFragmentData, FragmentChunk, ManifestChunk, MeshSource} from 'neuroglancer/mesh/backend'; -import {getGrapheneFragmentKey, responseIdentity} from 'neuroglancer/datasource/graphene/base'; +import {getGrapheneFragmentKey, GRAPHENE_REFRESH_MESH_RPC_ID, responseIdentity} from 'neuroglancer/datasource/graphene/base'; import {CancellationToken} from 'neuroglancer/util/cancellation'; -import {isNotFoundError, responseArrayBuffer, responseJson} from 'neuroglancer/util/http_request'; +import {HttpError, isNotFoundError, responseArrayBuffer, responseJson} from 'neuroglancer/util/http_request'; import {cancellableFetchSpecialOk, SpecialProtocolCredentials, SpecialProtocolCredentialsProvider} from 'neuroglancer/util/special_protocol_request'; import {Uint64} from 'neuroglancer/util/uint64'; import {registerSharedObject} from 'neuroglancer/worker_rpc'; @@ -68,6 +68,14 @@ function getVerifiedFragmentPromise( cancellationToken); } +function reject404(url: string): Promise { + return new Promise((_f, r) => { + setTimeout(() => { + r(new HttpError(url, 404, "404", undefined)); + }, 1000); + }) +} + function getFragmentDownloadPromise( credentialsProvider: SpecialProtocolCredentialsProvider, chunk: FragmentChunk, @@ -77,6 +85,9 @@ function getFragmentDownloadPromise( if (parameters.sharding){ fragmentDownloadPromise = getVerifiedFragmentPromise(credentialsProvider, chunk, parameters, cancellationToken); } else { + if (Math.random() < 0.5) { + return reject404(`${parameters.fragmentUrl}/${chunk.fragmentId}`); + } fragmentDownloadPromise = cancellableFetchSpecialOk( credentialsProvider, `${parameters.fragmentUrl}/${chunk.fragmentId}`, {}, responseArrayBuffer, @@ -94,6 +105,17 @@ async function decodeDracoFragmentChunk( @registerSharedObject() export class GrapheneMeshSource extends (WithParameters(WithSharedCredentialsProviderCounterpart()(MeshSource), MeshSourceParameters)) { + chunksNotFound = new Map(); + + redownload(segment: Uint64) { + const segmentString = segment.toJSON(); + const segmentChunks = this.chunksNotFound.get(segmentString) || []; + this.chunksNotFound.delete(segmentString); + for (let chunk of segmentChunks) { + this.chunkManager.queueManager.updateChunkState(chunk, ChunkState.QUEUED); + } + } + async download(chunk: ManifestChunk, cancellationToken: CancellationToken) { const {parameters} = this; if (isBaseSegmentId(chunk.objectId, parameters.nBitsForLayerId)) { @@ -115,9 +137,13 @@ async function decodeDracoFragmentChunk( await decodeDracoFragmentChunk(chunk, response); } catch (e) { if (isNotFoundError(e)) { - chunk.source!.removeChunk(chunk); + const segmentString = chunk.manifestChunk?.objectId?.toJSON(); + if (segmentString) { + this.chunksNotFound.set(segmentString, this.chunksNotFound.get(segmentString) || []); + this.chunksNotFound.get(segmentString)!.push(chunk); + } } - Promise.reject(e); + throw e; } } @@ -421,3 +447,8 @@ registerRPC(CHUNKED_GRAPH_RENDER_LAYER_UPDATE_SOURCES_RPC_ID, function(x) { attachment.state!.displayDimensionRenderInfo = x.displayDimensionRenderInfo; layer.chunkManager.scheduleUpdateChunkPriorities(); }); + +registerRPC(GRAPHENE_REFRESH_MESH_RPC_ID, function(x) { + let obj = this.get(x.rpcId); + obj.redownload(Uint64.parseString(x.segment)); +}); diff --git a/src/neuroglancer/datasource/graphene/base.ts b/src/neuroglancer/datasource/graphene/base.ts index 1981464f40..d920db9345 100644 --- a/src/neuroglancer/datasource/graphene/base.ts +++ b/src/neuroglancer/datasource/graphene/base.ts @@ -20,6 +20,7 @@ import {ChunkLayoutOptions, makeSliceViewChunkSpecification, SliceViewChunkSourc import {DataType} from 'neuroglancer/sliceview/base'; export const PYCG_APP_VERSION = 1; +export const GRAPHENE_REFRESH_MESH_RPC_ID = 'GrapheneMeshSource:RefreshMesh'; export enum VolumeChunkEncoding { RAW, diff --git a/src/neuroglancer/datasource/graphene/frontend.ts b/src/neuroglancer/datasource/graphene/frontend.ts index 16ee5a8309..241db518ec 100644 --- a/src/neuroglancer/datasource/graphene/frontend.ts +++ b/src/neuroglancer/datasource/graphene/frontend.ts @@ -21,7 +21,7 @@ import {ChunkManager, WithParameters} from 'neuroglancer/chunk_manager/frontend' import {makeIdentityTransform} from 'neuroglancer/coordinate_transform'; import {WithCredentialsProvider} from 'neuroglancer/credentials_provider/chunk_source_frontend'; import {DataSource, DataSubsourceEntry, GetDataSourceOptions, RedirectError} from 'neuroglancer/datasource'; -import {MeshSource} from 'neuroglancer/mesh/frontend'; +import {MeshLayer, MeshSource, MultiscaleMeshLayer} from 'neuroglancer/mesh/frontend'; import {Owned} from 'neuroglancer/util/disposable'; import {mat4, vec3, vec4} from 'neuroglancer/util/geom'; import {HttpError, isNotFoundError, responseJson} from 'neuroglancer/util/http_request'; @@ -29,7 +29,7 @@ import {parseArray, parseFixedLengthArray, verifyEnumString, verifyFiniteFloat, import {getObjectId} from 'neuroglancer/util/object_id'; import {cancellableFetchSpecialOk, parseSpecialUrl, SpecialProtocolCredentials, SpecialProtocolCredentialsProvider} from 'neuroglancer/util/special_protocol_request'; import {Uint64} from 'neuroglancer/util/uint64'; -import {getGrapheneFragmentKey, isBaseSegmentId, responseIdentity} from 'neuroglancer/datasource/graphene/base'; +import {getGrapheneFragmentKey, GRAPHENE_REFRESH_MESH_RPC_ID, isBaseSegmentId, responseIdentity} from 'neuroglancer/datasource/graphene/base'; import {ChunkedGraphSourceParameters, MeshSourceParameters, MultiscaleMeshMetadata, PYCG_APP_VERSION} from 'neuroglancer/datasource/graphene/base'; import {DataEncoding, ShardingHashFunction, ShardingParameters} from 'neuroglancer/datasource/precomputed/base'; import {StatusMessage} from 'neuroglancer/status'; @@ -647,6 +647,13 @@ class GraphConnection extends SegmentationGraphSourceConnection { public state: GrapheneState) { super(graph, layer.displayState.segmentationGroupState.value); const segmentsState = layer.displayState.segmentationGroupState.value; + segmentsState.selectedSegments.changed.add((segmentIds: Uint64[]|Uint64|null, add: boolean) => { + if (segmentIds !== null) { + segmentIds = Array().concat(segmentIds); + } + this.selectedSegmentsChanged(segmentIds, add); + }); + segmentsState.visibleSegments.changed.add((segmentIds: Uint64[]|Uint64|null, add: boolean) => { if (segmentIds !== null) { segmentIds = Array().concat(segmentIds); @@ -681,44 +688,35 @@ class GraphConnection extends SegmentationGraphSourceConnection { private visibleSegmentsChanged(segments: Uint64[]|null, added: boolean) { const {segmentsState} = this; - + const {focusSegment: {value: focusSegment}} = this.graph.state.multicutState; + if (focusSegment && !segmentsState.visibleSegments.has(focusSegment)) { + if (segmentsState.selectedSegments.has(focusSegment)) { + StatusMessage.showTemporaryMessage(`Can't hide active multicut segment.`, 3000); + } else { + StatusMessage.showTemporaryMessage(`Can't deselect active multicut segment.`, 3000); + } + segmentsState.selectedSegments.add(focusSegment); + segmentsState.visibleSegments.add(focusSegment); + if (segments) { + segments = segments.filter(segment => !Uint64.equal(segment, focusSegment)); + } + } if (segments === null) { - const leafSegmentCount = this.segmentsState.visibleSegments.size; + const leafSegmentCount = this.segmentsState.selectedSegments.size; this.segmentsState.segmentEquivalences.clear(); - StatusMessage.showTemporaryMessage(`Deselected all ${leafSegmentCount} segments.`, 3000); + StatusMessage.showTemporaryMessage(`Hid all ${leafSegmentCount} segments.`, 3000); return; } - for (const segmentId of segments) { - const isBaseSegment = isBaseSegmentId(segmentId, this.graph.info.graph.nBitsForLayerId); - - const segmentConst = segmentId.clone(); - - if (added) { - if (isBaseSegment) { - this.graph.getRoot(segmentConst).then(rootId => { - segmentsState.visibleSegments.delete(segmentConst); - segmentsState.visibleSegments.add(rootId); - }); - } - } else if (!isBaseSegment) { - const {focusSegment: {value: focusSegment}} = this.graph.state.multicutState; - if (focusSegment && Uint64.equal(segmentId, focusSegment)) { - segmentsState.visibleSegments.add(segmentId); - StatusMessage.showTemporaryMessage(`Can't deselect active multicut segment.`, 3000); - return; - } - + if (!added) { const segmentCount = [...segmentsState.segmentEquivalences.setElements(segmentId)].length; // Approximation - segmentsState.segmentEquivalences.deleteSet(segmentId); - if (this.lastDeselectionMessage && this.lastDeselectionMessageExists) { this.lastDeselectionMessage.dispose(); this.lastDeselectionMessageExists = false; } this.lastDeselectionMessage = - StatusMessage.showMessage(`Deselected ${segmentCount} segments.`); + StatusMessage.showMessage(`Hid ${segmentCount} segments.`); this.lastDeselectionMessageExists = true; setTimeout(() => { if (this.lastDeselectionMessageExists) { @@ -729,6 +727,30 @@ class GraphConnection extends SegmentationGraphSourceConnection { } } } + + private selectedSegmentsChanged(segments: Uint64[]|null, added: boolean) { + const {segmentsState} = this; + if (segments === null) { + const leafSegmentCount = this.segmentsState.selectedSegments.size; + StatusMessage.showTemporaryMessage(`Deselected all ${leafSegmentCount} segments.`, 3000); + return; + } + for (const segmentId of segments) { + const isBaseSegment = isBaseSegmentId(segmentId, this.graph.info.graph.nBitsForLayerId); + const segmentConst = segmentId.clone(); + if (added) { + if (isBaseSegment) { + this.graph.getRoot(segmentConst).then(rootId => { + if (segmentsState.visibleSegments.has(segmentConst)) { + segmentsState.visibleSegments.add(rootId); + } + segmentsState.selectedSegments.delete(segmentConst); + segmentsState.selectedSegments.add(rootId); + }); + } + } + } + } computeSplit(include: Uint64, exclude: Uint64): ComputedSplit|undefined { include; @@ -751,7 +773,11 @@ class GraphConnection extends SegmentationGraphSourceConnection { const focusSegment = multicutState.focusSegment.value!; multicutState.reset(); // need to clear the focus segment before deleting the multicut segment const {segmentsState} = this; - segmentsState.visibleSegments.delete(focusSegment); + segmentsState.selectedSegments.delete(focusSegment); + for (const segment of [...sinks, ...sources]) { + segmentsState.selectedSegments.delete(segment.rootId); + } + segmentsState.selectedSegments.add(splitRoots); segmentsState.visibleSegments.add(splitRoots); return true; } @@ -908,6 +934,11 @@ class GrapheneGraphSource extends SegmentationGraphSource { label: 'Merge', title: 'Merge segments' })); + toolbox.appendChild(makeToolButton(context, layer, { + toolJson: GRAPHENE_REFRESH_MESH_TOOL_ID, + label: 'Refresh Mesh', + title: 'Refresh Meshes' + })); parent.appendChild(toolbox); parent.appendChild( context.registerDisposer(new MulticutAnnotationLayerView(layer, layer.annotationDisplayState)) @@ -1048,6 +1079,7 @@ class SliceViewPanelChunkedGraphLayer extends SliceViewPanelRenderLayer { const GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID = 'grapheneMulticutSegments'; const GRAPHENE_MERGE_SEGMENTS_TOOL_ID = 'grapheneMergeSegments'; +const GRAPHENE_REFRESH_MESH_TOOL_ID = 'grapheneRefreshMesh'; class MulticutAnnotationLayerView extends AnnotationLayerView { private _annotationStates: MergedAnnotationStates; @@ -1261,7 +1293,7 @@ class MulticutSegmentsTool extends Tool { activation.bindAction('set-anchor', event => { event.stopPropagation(); - const currentSegmentSelection = maybeGetSelection(this, segmentationGroupState.visibleSegments); + const currentSegmentSelection = maybeGetSelection(this, segmentationGroupState.visibleSegments); // or visible segments? if (!currentSegmentSelection) return; const {rootId, segmentId} = currentSegmentSelection; const {focusSegment, segments} = multicutState; @@ -1397,9 +1429,10 @@ class MergeSegmentsTool extends Tool { const loadedSubsource = getGraphLoadedSubsource(this.layer)!; const annotationToNanometers = loadedSubsource.loadedDataSource.transform.inputSpace.value.scales.map(x => x / 1e-9); const mergedRoot = await graph.graphServer.mergeSegments(lastSegmentSelection, selection, annotationToNanometers); - const {visibleSegments} = segmentationGroupState; - visibleSegments.delete(lastSegmentSelection.rootId); - visibleSegments.delete(selection.rootId); + const {selectedSegments, visibleSegments} = segmentationGroupState; + selectedSegments.delete(lastSegmentSelection.rootId); + selectedSegments.delete(selection.rootId); + selectedSegments.add(mergedRoot); visibleSegments.add(mergedRoot); this.lastAnchorSelection.value = undefined; activation.cancel(); @@ -1425,3 +1458,52 @@ registerLayerTool(SegmentationUserLayer, GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID, lay registerLayerTool(SegmentationUserLayer, GRAPHENE_MERGE_SEGMENTS_TOOL_ID, layer => { return new MergeSegmentsTool(layer, true); }); + +registerLayerTool(SegmentationUserLayer, GRAPHENE_REFRESH_MESH_TOOL_ID, layer => { + return new RefreshMeshTool(layer); +}); + +const REFRESH_MESH_INPUT_EVENT_MAP = EventActionMap.fromObject({ + 'at:shift?+mousedown0': {action: 'refresh-mesh'}, +}); + +class RefreshMeshTool extends Tool { + activate(activation: ToolActivation) { + const {body, header} = makeToolActivationStatusMessageWithHeader(activation); + header.textContent = 'Refresh mesh'; + body.classList.add('neuroglancer-merge-segments-status'); + + activation.bindInputEventMap(REFRESH_MESH_INPUT_EVENT_MAP); // has to be after makeToolActivationStatusMessageWithHeader + + + const someMeshLayer = (layer: SegmentationUserLayer) => { + for (let x of layer.renderLayers) { + if (x instanceof MeshLayer || x instanceof MultiscaleMeshLayer) { + return x; + } + } + return undefined; + }; + + activation.bindAction('refresh-mesh', event => { + event.stopPropagation(); + const {segmentSelectionState, segmentationGroupState} = this.layer.displayState; + if (!segmentSelectionState.hasSelectedSegment) return; + const segment = segmentSelectionState.selectedSegment; + const {visibleSegments} = segmentationGroupState.value; + if (!visibleSegments.has(segment)) return; + const meshLayer = someMeshLayer(this.layer); + if (!meshLayer) return; + const meshSource = meshLayer.source; + meshSource.rpc!.invoke(GRAPHENE_REFRESH_MESH_RPC_ID, {'rpcId': meshSource.rpcId!, 'segment': segment.toString()}); + }); + } + + toJSON() { + return GRAPHENE_REFRESH_MESH_TOOL_ID; + } + + get description() { + return `refresh mesh`; + } +} diff --git a/src/neuroglancer/datasource/nggraph/frontend.ts b/src/neuroglancer/datasource/nggraph/frontend.ts index e62eb2df08..4e120e3a77 100644 --- a/src/neuroglancer/datasource/nggraph/frontend.ts +++ b/src/neuroglancer/datasource/nggraph/frontend.ts @@ -218,9 +218,12 @@ class GraphConnection extends SegmentationGraphSourceConnection { try { this.ignoreVisibleSegmentsChanged = true; if (this.segmentsState.visibleSegments.has(oldId)) { - this.segmentsState.visibleSegments.delete(oldId); this.segmentsState.visibleSegments.add(newId); } + if (this.segmentsState.selectedSegments.has(oldId)) { + this.segmentsState.selectedSegments.delete(oldId); + this.segmentsState.selectedSegments.add(newId); + } if (this.segmentsState.temporaryVisibleSegments.has(oldId)) { this.segmentsState.temporaryVisibleSegments.delete(oldId); this.segmentsState.temporaryVisibleSegments.add(newId); diff --git a/src/neuroglancer/segmentation_display_state/backend.ts b/src/neuroglancer/segmentation_display_state/backend.ts index 08277090a2..223635ca8a 100644 --- a/src/neuroglancer/segmentation_display_state/backend.ts +++ b/src/neuroglancer/segmentation_display_state/backend.ts @@ -43,6 +43,7 @@ export const withSegmentationLayerBackendState = >(Base: TBase) => class SegmentationLayerState extends Base implements VisibleSegmentsState { visibleSegments: Uint64Set; + selectedSegments: Uint64Set; segmentEquivalences: SharedDisjointUint64Sets; temporaryVisibleSegments: Uint64Set; temporarySegmentEquivalences: SharedDisjointUint64Sets; diff --git a/src/neuroglancer/segmentation_display_state/base.ts b/src/neuroglancer/segmentation_display_state/base.ts index 282358276d..033ffb4d11 100644 --- a/src/neuroglancer/segmentation_display_state/base.ts +++ b/src/neuroglancer/segmentation_display_state/base.ts @@ -23,6 +23,7 @@ import {VisibleSegmentEquivalencePolicy} from 'neuroglancer/segmentation_graph/s export interface VisibleSegmentsState { visibleSegments: Uint64Set; + selectedSegments: Uint64Set; segmentEquivalences: SharedDisjointUint64Sets; // Specifies a temporary/alternative set of segments/equivalences to use for display purposes, diff --git a/src/neuroglancer/segmentation_display_state/frontend.ts b/src/neuroglancer/segmentation_display_state/frontend.ts index a3b36b43d0..024fa8611b 100644 --- a/src/neuroglancer/segmentation_display_state/frontend.ts +++ b/src/neuroglancer/segmentation_display_state/frontend.ts @@ -40,6 +40,7 @@ import {Uint64} from 'neuroglancer/util/uint64'; import {withSharedVisibility} from 'neuroglancer/visibility_priority/frontend'; import {makeCopyButton} from 'neuroglancer/widget/copy_button'; import {makeFilterButton} from 'neuroglancer/widget/filter_button'; +import {makeStarButton} from 'neuroglancer/widget/star_button'; export class Uint64MapEntry { constructor(public key: Uint64, public value?: Uint64, public label?: string|undefined) {} @@ -270,6 +271,13 @@ const segmentWidgetTemplate = (() => { idElement.classList.add('neuroglancer-segment-list-entry-id'); const idIndex = idContainer.childElementCount; idContainer.appendChild(idElement); + const starButton = makeStarButton({ + title: `Star segment`, + }); + starButton.classList.add('neuroglancer-segment-list-entry-star'); + const starIndex = stickyContainer.childElementCount; + stickyContainer.appendChild(starButton); + const nameElement = document.createElement('span'); nameElement.classList.add('neuroglancer-segment-list-entry-name'); const labelIndex = template.childElementCount; @@ -289,6 +297,7 @@ const segmentWidgetTemplate = (() => { idIndex, labelIndex, filterIndex, + starIndex, unmappedIdIndex: -1, unmappedCopyIndex: -1 }; @@ -381,8 +390,12 @@ function makeRegisterSegmentWidgetEventHandlers(displayState: SegmentationDispla const idString = entryElement.dataset.id!; const id = tempStatedColor; id.tryParseString(idString); - const {visibleSegments} = displayState.segmentationGroupState.value; - visibleSegments.set(id, !visibleSegments.has(id)); + const {selectedSegments, visibleSegments} = displayState.segmentationGroupState.value; + const shouldBeVisible = !visibleSegments.has(id); + if (shouldBeVisible) { + selectedSegments.add(id); + } + visibleSegments.set(id, shouldBeVisible); event.stopPropagation(); }; @@ -421,6 +434,16 @@ function makeRegisterSegmentWidgetEventHandlers(displayState: SegmentationDispla stickyChildren[template.visibleIndex].addEventListener('click', visibleCheckboxHandler); children[template.filterIndex].addEventListener('click', filterHandler); element.addEventListener('action:select-position', selectHandler); + + const starButton = stickyChildren[template.starIndex] as HTMLElement; + starButton.addEventListener('click', (event: MouseEvent) => { + const entryElement = getEntryElement(event); + const idString = entryElement.dataset.id!; + const id = tempStatedColor + id.tryParseString(idString); + const {selectedSegments} = displayState.segmentationGroupState.value; + selectedSegments.set(id, !selectedSegments.has(id)); + }); }; } @@ -511,6 +534,8 @@ export class SegmentWidgetFactory