diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index ccbbfc145df..9d20c6fa749 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,9 +11,11 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/21.02.0...HEAD) ### Added +- The "Meshes" tab was overhauled, so that it displays generated isosurfaces and imported meshes. Generated isosurfaces can be jumped to, reloaded, downloaded and removed. [#4917](https://github.com/scalableminds/webknossos/pull/4917) - Added an explicit `/signup` (or `/auth/signup`) route. [#5091](https://github.com/scalableminds/webknossos/pull/5091/files) ### Changed +- Make the isosurface feature in the meshes tab more robust. If a request fails, a retry is initiated. [#5102](https://github.com/scalableminds/webknossos/pull/5102) - Support for the old invite links was removed. These contained the organization name in the URL. The new links contain a token (can be generated in the users view). For instances with a single organization the old invite links should still work. [#5091](https://github.com/scalableminds/webknossos/pull/5091/files) - Users are no longer allowed to deactivate their own accounts. [#5070](https://github.com/scalableminds/webknossos/pull/5070) @@ -21,4 +23,4 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - ### Removed -- +- The isosurface setting was removed. Instead, isosurfaces can be generated via the "Meshes" tab. Also note that the Shift+Click binding for generating an isosurface was removed (for now). Please refer to the "Meshes" tab, too. [#4917](https://github.com/scalableminds/webknossos/pull/4917) diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 9472b1ca4fc..72e6463a9e8 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -116,22 +116,26 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { case "ADD_ISOSURFACE": { const { cellId, seedPosition } = action; - return updateKey2(state, "isosurfaces", cellId.toString(), { + // $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries + return updateKey2(state, "isosurfaces", cellId, { segmentId: cellId, seedPosition, + isLoading: false, }); } case "START_REFRESHING_ISOSURFACE": { const { cellId } = action; - return updateKey2(state, "isosurfaces", cellId.toString(), { + // $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries + return updateKey2(state, "isosurfaces", cellId, { isLoading: true, }); } case "FINISHED_REFRESHING_ISOSURFACE": { const { cellId } = action; - return updateKey2(state, "isosurfaces", cellId.toString(), { + // $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries + return updateKey2(state, "isosurfaces", cellId, { isLoading: false, }); } diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 1cd1846d47b..4492244f01a 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -1,6 +1,8 @@ // @flow import { saveAs } from "file-saver"; +import { sleep } from "libs/utils"; +import ErrorHandling from "libs/error_handling"; import type { APIDataset } from "types/api_flow_types"; import { ResolutionInfo, getResolutionInfo } from "oxalis/model/accessors/dataset_accessor"; import { @@ -46,6 +48,9 @@ import { saveNowAction } from "oxalis/model/actions/save_actions"; import Toast from "libs/toast"; import messages from "messages"; +const MAX_RETRY_COUNT = 5; +const RETRY_WAIT_TIME = 5000; + const isosurfacesMap: Map> = new Map(); const cubeSize = [256, 256, 256]; const modifiedCells: Set = new Set(); @@ -207,9 +212,13 @@ function* loadIsosurfaceWithNeighbors( seedPosition != null ? seedPosition : yield* select(state => getFlooredPosition(state.flycam)); const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo); let positionsToRequest = [clippedPosition]; - if (seedPosition) { - yield* put(addIsosurfaceAction(segmentId, seedPosition)); + + const hasIsosurface = yield* select(state => state.isosurfaces[segmentId] != null); + if (!hasIsosurface) { + yield* put(addIsosurfaceAction(segmentId, position)); } + yield* put(startRefreshingIsosurfaceAction(segmentId)); + while (positionsToRequest.length > 0) { const currentPosition = positionsToRequest.shift(); const neighbors = yield* call( @@ -225,6 +234,8 @@ function* loadIsosurfaceWithNeighbors( isInitialRequest = false; positionsToRequest = positionsToRequest.concat(neighbors); } + + yield* put(finishedRefreshingIsosurfaceAction(segmentId)); } function hasBatchCounterExceededLimit(segmentId: number): boolean { @@ -267,34 +278,46 @@ function* maybeLoadIsosurface( const volumeTracing = yield* select(state => state.tracing.volume); // Fetch from datastore if no volumetracing exists or if the tracing has a fallback layer. const useDataStore = volumeTracing == null || volumeTracing.fallbackLayer != null; - const { buffer: responseBuffer, neighbors } = yield* call( - computeIsosurface, - useDataStore ? dataStoreUrl : tracingStoreUrl, - layer, - { - position: clippedPosition, - zoomStep, - segmentId, - voxelDimensions, - cubeSize, - scale, - }, - ); - // Check again whether the limit was exceeded, since this variable could have been - // set in the mean time by ctrl-clicking the segment to remove it - if (hasBatchCounterExceededLimit(segmentId)) { - return []; - } - const vertices = new Float32Array(responseBuffer); - if (removeExistingIsosurface) { - getSceneController().removeIsosurfaceById(segmentId); + let retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + try { + const { buffer: responseBuffer, neighbors } = yield* call( + computeIsosurface, + useDataStore ? dataStoreUrl : tracingStoreUrl, + layer, + { + position: clippedPosition, + zoomStep, + segmentId, + voxelDimensions, + cubeSize, + scale, + }, + ); + + // Check again whether the limit was exceeded, since this variable could have been + // set in the mean time by ctrl-clicking the segment to remove it + if (hasBatchCounterExceededLimit(segmentId)) { + return []; + } + const vertices = new Float32Array(responseBuffer); + if (removeExistingIsosurface) { + getSceneController().removeIsosurfaceById(segmentId); + } + getSceneController().addIsosurfaceFromVertices(vertices, segmentId); + + return neighbors.map(neighbor => + getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo), + ); + } catch (exception) { + retryCount++; + ErrorHandling.notify(exception); + console.warn("Retrying isosurface generation..."); + yield* call(sleep, RETRY_WAIT_TIME * 2 ** retryCount); + } } - getSceneController().addIsosurfaceFromVertices(vertices, segmentId); - - return neighbors.map(neighbor => - getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo), - ); + return []; } function* downloadIsosurfaceCellById(cellId: number): Saga { @@ -334,7 +357,11 @@ function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga getFlooredPosition(state.flycam)); + yield* put(addIsosurfaceAction(segmentId, seedPosition)); } function* removeIsosurface( diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index 08a17bbf4f4..a4206803f49 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -453,11 +453,11 @@ type UiInformation = { +isRefreshingIsosurfaces: boolean, }; -type IsosurfaceInformation = { +export type IsosurfaceInformation = {| +segmentId: number, +seedPosition: Vector3, +isLoading: boolean, -}; +|}; export type OxalisState = {| +datasetConfiguration: DatasetConfiguration, @@ -471,7 +471,7 @@ export type OxalisState = {| +viewModeData: ViewModeData, +activeUser: ?APIUser, +uiInformation: UiInformation, - +isosurfaces: { [segmentId: string]: IsosurfaceInformation }, + +isosurfaces: { [segmentId: number]: IsosurfaceInformation }, |}; const sagaMiddleware = createSagaMiddleware(); diff --git a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js index d91f2de2619..5d3ac465543 100644 --- a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js @@ -8,7 +8,7 @@ import _ from "lodash"; import type { ExtractReturn } from "libs/type_helpers"; import type { MeshMetaData, RemoteMeshMetaData } from "types/api_flow_types"; -import type { OxalisState } from "oxalis/store"; +import type { OxalisState, IsosurfaceInformation } from "oxalis/store"; import Store from "oxalis/store"; import Model from "oxalis/model"; import type { Vector3 } from "oxalis/constants"; @@ -137,7 +137,7 @@ type StateProps = {| |}; type DispatchProps = ExtractReturn; -type Props = { ...OwnProps, ...DispatchProps, ...StateProps }; +type Props = {| ...OwnProps, ...DispatchProps, ...StateProps |}; const getCheckboxStyle = isLoaded => isLoaded @@ -263,10 +263,11 @@ class MeshesView extends React.Component< ); - const renderIsosurfaceListItem = (isosurface: Object) => { + const renderIsosurfaceListItem = (isosurface: IsosurfaceInformation) => { const { segmentId, seedPosition, isLoading } = isosurface; const centeredCell = getIdForPos(getPosition(this.props.flycam)); - const actionVisibility = segmentId === this.state.hoveredListItem ? "visible" : "hidden"; + const actionVisibility = + isLoading || segmentId === this.state.hoveredListItem ? "visible" : "hidden"; return (