diff --git a/.prettierrc b/.prettierrc index 978a71006f0..6ae999b7f3f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,7 @@ { "files": "effect-generators.js.flow", "options": { - "printWidth": 200 + "printWidth": 240 } } ] diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 69fe8c081c2..617ee3b6b22 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/20.07.0...HEAD) ### Added +- Isosurface generation now also supports volume tracings without fallback layer. [#4567](https://github.com/scalableminds/webknossos/pull/4567) - Added a tool to initiate a flood fill in a volume tracing with the active cell id. [#4780](https://github.com/scalableminds/webknossos/pull/4780) - Added the possibility to merge volume tracings both via file upload (zip of zips) and when viewing projects/tasks as compound annotations. [#4709](https://github.com/scalableminds/webknossos/pull/4709) - Added the possibility to remove the fallback segmentation layer from a hybrid/volume tracing. Accessible by a minus button next to the layer's settings. [#4741](https://github.com/scalableminds/webknossos/pull/4766) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index ada9f6207fe..1147e2b8a18 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -1251,20 +1251,18 @@ type IsosurfaceRequest = { segmentId: number, voxelDimensions: Vector3, cubeSize: Vector3, + scale: Vector3, }; export function computeIsosurface( - datastoreUrl: string, - datasetId: APIDatasetId, + requestUrl: string, layer: DataLayer, isosurfaceRequest: IsosurfaceRequest, ): Promise<{ buffer: ArrayBuffer, neighbors: Array }> { - const { position, zoomStep, segmentId, voxelDimensions, cubeSize } = isosurfaceRequest; + const { position, zoomStep, segmentId, voxelDimensions, cubeSize, scale } = isosurfaceRequest; return doWithToken(async token => { const { buffer, headers } = await Request.sendJSONReceiveArraybufferWithHeaders( - `${datastoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${ - layer.fallbackLayer != null ? layer.fallbackLayer : layer.name - }/isosurface?token=${token}`, + `${requestUrl}/isosurface?token=${token}`, { data: { // The back-end needs a small padding at the border of the @@ -1280,6 +1278,7 @@ export function computeIsosurface( mappingType: layer.activeMappingType, // "size" of each voxel (i.e., only every nth voxel is considered in each dimension) voxelDimensions, + scale, }, }, ); diff --git a/frontend/javascripts/libs/ThreeDMap.js b/frontend/javascripts/libs/ThreeDMap.js index 3055e822aca..fc3f3942364 100644 --- a/frontend/javascripts/libs/ThreeDMap.js +++ b/frontend/javascripts/libs/ThreeDMap.js @@ -44,6 +44,24 @@ export default class ThreeDMap { .set(z, value); } + entries(): Array<[T, Vector3]> { + const entries: Array<[T, Vector3]> = []; + this.map.forEach((atX, x) => { + if (!atX) { + return; + } + atX.forEach((atY, y) => { + if (!atY) { + return; + } + atY.forEach((value, z) => { + entries.push([value, [x, y, z]]); + }); + }); + }); + return entries; + } + // This could be extended so the key is a Vector1 | Vector2 // if needed in the future delete(key: number): boolean { diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index 73306706958..dee7890d2a1 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -71,6 +71,7 @@ import { setTreeGroupsAction, } from "oxalis/model/actions/skeletontracing_actions"; import { setPositionAction, setRotationAction } from "oxalis/model/actions/flycam_actions"; +import { refreshIsosurfacesAction } from "oxalis/model/actions/annotation_actions"; import { updateUserSettingAction, updateDatasetSettingAction, @@ -821,6 +822,10 @@ class DataApi { return Store.getState().temporaryConfiguration.activeMapping.isMappingEnabled; } + refreshIsosurfaces() { + Store.dispatch(refreshIsosurfacesAction()); + } + /** * Returns the bounding box for a given layer name. */ diff --git a/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js new file mode 100644 index 00000000000..543780f2678 --- /dev/null +++ b/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js @@ -0,0 +1,33 @@ +// @flow +import { type OrthoView, type Point2 } from "oxalis/constants"; +import Model from "oxalis/model"; +import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions"; +import { calculateGlobalPos } from "oxalis/controller/viewmodes/plane_controller"; +import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; +import Store from "oxalis/store"; + +function isosurfaceLeftClick(pos: Point2, plane: OrthoView, event: MouseEvent) { + if (!event.shiftKey) { + return; + } + let cellId = 0; + const position = calculateGlobalPos(pos); + const volumeTracingMaybe = Store.getState().tracing.volume; + if (volumeTracingMaybe) { + cellId = volumeTracingMaybe.activeCellId; + } else { + const segmentation = Model.getSegmentationLayer(); + if (!segmentation) { + return; + } + cellId = segmentation.cube.getMappedDataValue( + position, + getRequestLogZoomStep(Store.getState()), + ); + } + if (cellId > 0) { + Store.dispatch(changeActiveIsosurfaceCellAction(cellId, position)); + } +} + +export default isosurfaceLeftClick; diff --git a/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js index 76687613f9e..dc73b9067cd 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js +++ b/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js @@ -51,6 +51,7 @@ import type { Edge, Tree, Node } from "oxalis/store"; import api from "oxalis/api/internal_api"; import getSceneController from "oxalis/controller/scene_controller_provider"; import { renderToTexture } from "oxalis/view/rendering_utils"; +import isosurfaceLeftClick from "oxalis/controller/combinations/segmentation_plane_controller"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import Dimensions from "oxalis/model/dimensions"; @@ -88,7 +89,7 @@ export function getPlaneMouseControls(planeView: PlaneView) { } }, leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => - onClick(planeView, pos, event.shiftKey, event.altKey, event.ctrlKey, plane, isTouch), + onClick(planeView, pos, event.shiftKey, event.altKey, event.ctrlKey, plane, isTouch, event), rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { const { volume } = Store.getState().tracing; if (!volume || volume.activeTool !== VolumeToolEnum.BRUSH) { @@ -278,8 +279,9 @@ function onClick( ctrlPressed: boolean, plane: OrthoView, isTouch: boolean, + event?: MouseEvent, ): void { - if (!shiftPressed && !isTouch) { + if (!shiftPressed && !isTouch && !(ctrlPressed && event != null)) { // do nothing return; } @@ -325,6 +327,8 @@ function onClick( } else { Store.dispatch(setActiveNodeAction(nodeId)); } + } else if (shiftPressed && event != null) { + isosurfaceLeftClick(position, plane, event); } } diff --git a/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js index e3334607dd4..67d3d137051 100644 --- a/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js +++ b/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js @@ -37,6 +37,7 @@ import { movePlaneFlycamOrthoAction, setPositionAction } from "oxalis/model/acti import Model from "oxalis/model"; import Store from "oxalis/store"; import * as Utils from "libs/utils"; +import isosurfaceLeftClick from "oxalis/controller/combinations/segmentation_plane_controller"; // TODO: Build proper UI for this window.isAutomaticBrushEnabled = false; @@ -169,6 +170,7 @@ export function getPlaneMouseControls(_planeId: OrthoView): * { ); if (cellId > 0) { Store.dispatch(setActiveCellAction(cellId)); + isosurfaceLeftClick(pos, plane, event); } } else if (event.shiftKey && event.ctrlKey) { Store.dispatch(floodFillAction(calculateGlobalPos(pos), plane)); diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 189d430cc99..2c52cc39378 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -186,6 +186,7 @@ class SceneController { .to({ opacity: 0.95 }, 500) .onUpdate(function onUpdate() { meshMaterial.opacity = this.opacity; + app.vent.trigger("rerender"); }) .start(); diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js index d83988a450e..8c153209531 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js @@ -10,7 +10,6 @@ import * as React from "react"; import _ from "lodash"; import { InputKeyboard, InputKeyboardNoLoop, InputMouse, type ModifierKeys } from "libs/input"; -import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions"; import { document } from "libs/window"; import { getBaseVoxel, getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import { getViewportScale, getInputCatcherRect } from "oxalis/model/accessors/view_mode_accessor"; @@ -51,6 +50,7 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import * as skeletonController from "oxalis/controller/combinations/skeletontracing_plane_controller"; import * as volumeController from "oxalis/controller/combinations/volumetracing_plane_controller"; import { downloadScreenshot } from "oxalis/view/rendering_utils"; +import isosurfaceLeftClick from "oxalis/controller/combinations/segmentation_plane_controller"; const MAX_BRUSH_CHANGE_VALUE = 5; const BRUSH_CHANGING_CONSTANT = 0.02; @@ -69,24 +69,6 @@ function ensureNonConflictingHandlers(skeletonControls: Object, volumeControls: } } -const isosurfaceLeftClick = (pos: Point2, plane: OrthoView, event: MouseEvent) => { - if (!event.shiftKey) { - return; - } - const segmentation = Model.getSegmentationLayer(); - if (!segmentation) { - return; - } - const position = calculateGlobalPos(pos); - const cellId = segmentation.cube.getMappedDataValue( - position, - getRequestLogZoomStep(Store.getState()), - ); - if (cellId > 0) { - Store.dispatch(changeActiveIsosurfaceCellAction(cellId, position)); - } -}; - type StateProps = {| tracing: Tracing, is2d: boolean, diff --git a/frontend/javascripts/oxalis/default_state.js b/frontend/javascripts/oxalis/default_state.js index 1178e383155..b05ad1f6d0a 100644 --- a/frontend/javascripts/oxalis/default_state.js +++ b/frontend/javascripts/oxalis/default_state.js @@ -203,6 +203,7 @@ const defaultState: OxalisState = { isImportingMesh: false, isInAnnotationView: false, hasOrganizations: false, + isRefreshingIsosurfaces: false, }, }; diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index 9359dfc3913..e8865a3bbd5 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -75,6 +75,14 @@ export type TriggerIsosurfaceDownloadAction = { type: "TRIGGER_ISOSURFACE_DOWNLOAD", }; +export type RefreshIsosurfacesAction = { + type: "REFRESH_ISOSURFACES", +}; + +export type FinishedRefreshingIsosurfacesAction = { + type: "FINISHED_REFRESHING_ISOSURFACES", +}; + export type ImportIsosurfaceFromStlAction = { type: "IMPORT_ISOSURFACE_FROM_STL", buffer: ArrayBuffer, @@ -99,6 +107,8 @@ export type AnnotationActionTypes = | CreateMeshFromBufferAction | UpdateLocalMeshMetaDataAction | TriggerIsosurfaceDownloadAction + | RefreshIsosurfacesAction + | FinishedRefreshingIsosurfacesAction | ImportIsosurfaceFromStlAction | RemoveIsosurfaceAction; @@ -192,6 +202,14 @@ export const triggerIsosurfaceDownloadAction = (): TriggerIsosurfaceDownloadActi type: "TRIGGER_ISOSURFACE_DOWNLOAD", }); +export const refreshIsosurfacesAction = (): RefreshIsosurfacesAction => ({ + type: "REFRESH_ISOSURFACES", +}); + +export const finishedRefreshingIsosurfacesAction = (): FinishedRefreshingIsosurfacesAction => ({ + type: "FINISHED_REFRESHING_ISOSURFACES", +}); + export const importIsosurfaceFromStlAction = ( buffer: ArrayBuffer, ): ImportIsosurfaceFromStlAction => ({ diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.js b/frontend/javascripts/oxalis/model/reducers/ui_reducer.js index 77407410d90..58dd830bc26 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.js @@ -31,6 +31,14 @@ function UiReducer(state: OxalisState, action: Action): OxalisState { return updateKey(state, "uiInformation", { hasOrganizations: action.value }); } + case "REFRESH_ISOSURFACES": { + return updateKey(state, "uiInformation", { isRefreshingIsosurfaces: true }); + } + + case "FINISHED_REFRESHING_ISOSURFACES": { + return updateKey(state, "uiInformation", { isRefreshingIsosurfaces: false }); + } + default: return state; } diff --git a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow index 30109de1612..ac6094722e3 100644 --- a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow +++ b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow @@ -29,6 +29,7 @@ declare type Fn3 = (t1: T1, t2: T2, t3: T3) => Promise | Gener declare type Fn4 = (t1: T1, t2: T2, t3: T3, t4: T4) => Promise | Generator<*, R, *> | R; declare type Fn5 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => Promise | Generator<*, R, *> | R; declare type Fn6 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => Promise | Generator<*, R, *> | R; +declare type Fn7 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7) => Promise | Generator<*, R, *> | R; /* ------------------ SELECT Stuff ------------------ */ @@ -59,7 +60,8 @@ declare type ContextCallFn = (>(cfn: [C, Fn], ...rest: (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, ...rest: Array) => Generator<*, R, *>) & (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, ...rest: Array) => Generator<*, R, *>) & (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, ...rest: Array) => Generator<*, R, *>) & - (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, R, *>); + (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, R, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, ...rest: Array) => Generator<*, R, *>); // & (>(cfn: [C, Fn], t1: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); declare type CallFn = ContextCallFn & @@ -69,7 +71,8 @@ declare type CallFn = ContextCallFn & (>(fn: Fn, t1: T1, t2: T2, t3: T3) => Generator<*, R, *>) & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4) => Generator<*, R, *>) & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => Generator<*, R, *>) & - (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => Generator<*, R, *>); + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7) => Generator<*, R, *>); // & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); /* ------------------ CPS Stuff ------------------ */ diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index ea0d265352d..490dd79de61 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -6,11 +6,13 @@ import { changeActiveIsosurfaceCellAction, type ChangeActiveIsosurfaceCellAction, } from "oxalis/model/actions/segmentation_actions"; -import { ControlModeEnum, type Vector3 } from "oxalis/constants"; +import { type Vector3 } from "oxalis/constants"; import { type FlycamAction, FlycamActions } from "oxalis/model/actions/flycam_actions"; -import type { - ImportIsosurfaceFromStlAction, - RemoveIsosurfaceAction, +import { + removeIsosurfaceAction, + finishedRefreshingIsosurfacesAction, + type ImportIsosurfaceFromStlAction, + type RemoveIsosurfaceAction, } from "oxalis/model/actions/annotation_actions"; import { type Saga, @@ -33,11 +35,14 @@ import exportToStl from "libs/stl_exporter"; import getSceneController from "oxalis/controller/scene_controller_provider"; import parseStlBuffer from "libs/parse_stl_buffer"; import window from "libs/window"; +import { enforceVolumeTracing } from "oxalis/model/accessors/volumetracing_accessor"; +import { saveNowAction } from "oxalis/model/actions/save_actions"; import Toast from "libs/toast"; import messages from "messages"; const isosurfacesMap: Map> = new Map(); const cubeSize = [256, 256, 256]; +const modifiedCells: Set = new Set(); export function isIsosurfaceStl(buffer: ArrayBuffer): boolean { const dataView = new DataView(buffer); @@ -112,7 +117,7 @@ const MAXIMUM_BATCH_SIZE = 50; function* changeActiveIsosurfaceCell(action: ChangeActiveIsosurfaceCellAction): Saga { currentViewIsosurfaceCellId = action.cellId; - yield* call(ensureSuitableIsosurface, null, action.seedPosition); + yield* call(ensureSuitableIsosurface, null, action.seedPosition, currentViewIsosurfaceCellId); } // This function either returns the activeCellId of the current volume tracing @@ -129,17 +134,15 @@ function* getCurrentCellId(): Saga { function* ensureSuitableIsosurface( maybeFlycamAction: ?FlycamAction, seedPosition?: Vector3, + cellId?: number, + removeExistingIsosurface: boolean = false, ): Saga { - const segmentId = yield* call(getCurrentCellId); + const segmentId = cellId != null ? cellId : currentViewIsosurfaceCellId; if (segmentId === 0) { return; } const renderIsosurfaces = yield* select(state => state.datasetConfiguration.renderIsosurfaces); - const isControlModeSupported = yield* select( - state => - state.temporaryConfiguration.controlMode === ControlModeEnum.VIEW || window.allowIsosurfaces, - ); - if (!renderIsosurfaces || !isControlModeSupported) { + if (!renderIsosurfaces) { return; } const dataset = yield* select(state => state.dataset); @@ -164,6 +167,7 @@ function* ensureSuitableIsosurface( clippedPosition, zoomStep, resolutions, + removeExistingIsosurface, ); } @@ -174,9 +178,10 @@ function* loadIsosurfaceWithNeighbors( clippedPosition: Vector3, zoomStep: number, resolutions: Array, + removeExistingIsosurface: boolean, ): Saga { + let isInitialRequest = true; let positionsToRequest = [clippedPosition]; - while (positionsToRequest.length > 0) { const position = positionsToRequest.shift(); const neighbors = yield* call( @@ -187,7 +192,9 @@ function* loadIsosurfaceWithNeighbors( position, zoomStep, resolutions, + removeExistingIsosurface && isInitialRequest, ); + isInitialRequest = false; positionsToRequest = positionsToRequest.concat(neighbors); } } @@ -205,6 +212,7 @@ function* maybeLoadIsosurface( clippedPosition: Vector3, zoomStep: number, resolutions: Array, + removeExistingIsosurface: boolean, ): Saga> { const threeDMap = getMapForSegment(segmentId); @@ -219,12 +227,21 @@ function* maybeLoadIsosurface( threeDMap.set(clippedPosition, true); const voxelDimensions = window.__isosurfaceVoxelDimensions || [4, 4, 4]; + const scale = yield* select(state => state.dataset.dataSource.scale); const dataStoreHost = yield* select(state => state.dataset.dataStore.url); + const tracingStoreHost = yield* select(state => state.tracing.tracingStore.url); + const dataStoreUrl = `${dataStoreHost}/data/datasets/${dataset.owningOrganization}/${ + dataset.name + }/layers/${layer.fallbackLayer != null ? layer.fallbackLayer : layer.name}`; + const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; + + 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, - dataStoreHost, - dataset, + useDataStore ? dataStoreUrl : tracingStoreUrl, layer, { position: clippedPosition, @@ -232,6 +249,7 @@ function* maybeLoadIsosurface( segmentId, voxelDimensions, cubeSize, + scale, }, ); @@ -240,8 +258,10 @@ function* maybeLoadIsosurface( if (hasBatchCounterExceededLimit(segmentId)) { return []; } - const vertices = new Float32Array(responseBuffer); + if (removeExistingIsosurface) { + getSceneController().removeIsosurfaceById(segmentId); + } getSceneController().addIsosurfaceFromVertices(vertices, segmentId); return neighbors.map(neighbor => @@ -280,9 +300,14 @@ function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga { +function* removeIsosurface( + action: RemoveIsosurfaceAction, + removeFromScene: boolean = true, +): Saga { const { cellId } = action; - getSceneController().removeIsosurfaceById(cellId); + if (removeFromScene) { + getSceneController().removeIsosurfaceById(cellId); + } removeMapForSegment(cellId); // Set batch counter to maximum so that potentially running requests are aborted @@ -296,14 +321,63 @@ function* removeIsosurface(action: RemoveIsosurfaceAction): Saga { } } +function* markEditedCellAsDirty(): Saga { + const volumeTracing = yield* select(state => state.tracing.volume); + const useTracingStore = volumeTracing != null && volumeTracing.fallbackLayer == null; + if (useTracingStore) { + const activeCellId = yield* select(state => enforceVolumeTracing(state.tracing).activeCellId); + modifiedCells.add(activeCellId); + } +} + +function* refreshIsosurfaces(): Saga { + const renderIsosurfaces = yield* select(state => state.datasetConfiguration.renderIsosurfaces); + if (!renderIsosurfaces) { + return; + } + yield* put(saveNowAction()); + // We reload all cells that got modified till the start of reloading. + // By that we avoid that removing cells that got annotated during reloading from the modifiedCells set. + const currentlyModifiedCells = new Set(modifiedCells); + modifiedCells.clear(); + // First create an array containing information about all loaded isosurfaces as the map is manipulated within the loop. + for (const [cellId, threeDMap] of Array.from(isosurfacesMap.entries())) { + if (!currentlyModifiedCells.has(cellId)) { + continue; + } + const isosurfacePositions = threeDMap.entries().filter(([value, _position]) => value); + if (isosurfacePositions.length === 0) { + continue; + } + // Removing Isosurface from cache. + yield* call(removeIsosurface, removeIsosurfaceAction(cellId), false); + // The isosurface should only be removed once after re-fetching the isosurface first position. + let shouldBeRemoved = true; + for (const [, position] of isosurfacePositions) { + // Reload the Isosurface at the given position if it isn't already loaded there. + // This is done to ensure that every voxel of the isosurface is reloaded. + yield* call(ensureSuitableIsosurface, null, position, cellId, shouldBeRemoved); + shouldBeRemoved = false; + } + } + // Also load the Isosurface at the current flycam position. + const segmentationLayer = Model.getSegmentationLayer(); + if (!segmentationLayer) { + return; + } + const position = yield* select(state => getFlooredPosition(state.flycam)); + const cellIdAtFlycamPosition = segmentationLayer.cube.getDataValue(position); + yield* call(ensureSuitableIsosurface, null, position, cellIdAtFlycamPosition); + yield* put(finishedRefreshingIsosurfacesAction()); +} + export default function* isosurfaceSaga(): Saga { yield* take("WK_READY"); yield _takeEvery(FlycamActions, ensureSuitableIsosurface); - yield _takeEvery( - ["CHANGE_ACTIVE_ISOSURFACE_CELL", "SET_ACTIVE_CELL"], - changeActiveIsosurfaceCell, - ); + yield _takeEvery("CHANGE_ACTIVE_ISOSURFACE_CELL", changeActiveIsosurfaceCell); yield _takeEvery("TRIGGER_ISOSURFACE_DOWNLOAD", downloadActiveIsosurfaceCell); yield _takeEvery("IMPORT_ISOSURFACE_FROM_STL", importIsosurfaceFromStl); yield _takeEvery("REMOVE_ISOSURFACE", removeIsosurface); + yield _takeEvery("REFRESH_ISOSURFACES", refreshIsosurfaces); + yield _takeEvery(["START_EDITING", "COPY_SEGMENTATION_LAYER"], markEditedCellAsDirty); } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index ffd9303afb6..11a20a44123 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -31,7 +31,11 @@ import { enforceVolumeTracing, isVolumeTracingDisallowed, } from "oxalis/model/accessors/volumetracing_accessor"; -import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { + getPosition, + getFlooredPosition, + getRotation, +} from "oxalis/model/accessors/flycam_accessor"; import { type BoundingBoxType, type ContourMode, @@ -147,7 +151,7 @@ export function* editVolumeLayerAsync(): Generator { } function* getBoundingsFromPosition(currentViewport: OrthoView): Saga { - const position = Dimensions.roundCoordinate(yield* select(state => getPosition(state.flycam))); + const position = yield* select(state => getFlooredPosition(state.flycam)); const halfViewportExtents = yield* call(getHalfViewportExtents, currentViewport); const halfViewportExtentsUVW = Dimensions.transDim([...halfViewportExtents, 0], currentViewport); return { @@ -157,7 +161,7 @@ function* getBoundingsFromPosition(currentViewport: OrthoView): Saga { - const position = Dimensions.roundCoordinate(yield* select(state => getPosition(state.flycam))); + const position = yield* select(state => getFlooredPosition(state.flycam)); const thirdDimValue = position[Dimensions.thirdDimensionForPlane(planeId)]; return new VolumeLayer(planeId, thirdDimValue); } @@ -198,7 +202,7 @@ function* copySegmentationLayer(action: CopySegmentationLayerAction): Saga } const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); - const position = Dimensions.roundCoordinate(yield* select(state => getPosition(state.flycam))); + const position = yield* select(state => getFlooredPosition(state.flycam)); const [halfViewportExtentX, halfViewportExtentY] = yield* call( getHalfViewportExtents, activeViewport, diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index 55db6691891..a4ad4e1fcb5 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -448,6 +448,7 @@ type UiInformation = { +isImportingMesh: boolean, +isInAnnotationView: boolean, +hasOrganizations: boolean, + +isRefreshingIsosurfaces: boolean, }; export type OxalisState = {| diff --git a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js index b1de3dc2bc8..0f5720994f0 100644 --- a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js @@ -46,12 +46,7 @@ import Store, { } from "oxalis/store"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; -import constants, { - type ControlMode, - ControlModeEnum, - type ViewMode, - type Vector3, -} from "oxalis/constants"; +import constants, { type ViewMode, type Vector3 } from "oxalis/constants"; import Histogram, { isHistogramSupported } from "./histogram_view"; @@ -70,7 +65,6 @@ type DatasetSettingsProps = {| ) => void, viewMode: ViewMode, histogramData: HistogramDataForAllLayers, - controlMode: ControlMode, onSetPosition: Vector3 => void, onZoomToResolution: Vector3 => number, onChangeUser: (key: $Keys, value: any) => void, @@ -395,8 +389,7 @@ class DatasetSettings extends React.PureComponent { onChange={_.partial(this.props.onChange, "highlightHoveredCellId")} /> )} - {!isColorLayer && - (this.props.controlMode === ControlModeEnum.VIEW || window.allowIsosurfaces) ? ( + {!isColorLayer ? ( ({ datasetConfiguration: state.datasetConfiguration, viewMode: state.temporaryConfiguration.viewMode, histogramData: state.temporaryConfiguration.histogramData, - controlMode: state.temporaryConfiguration.controlMode, dataset: state.dataset, tracing: state.tracing, }); diff --git a/frontend/javascripts/oxalis/view/td_view_controls.js b/frontend/javascripts/oxalis/view/td_view_controls.js index d5d8b759f2c..5c1ae8c8faf 100644 --- a/frontend/javascripts/oxalis/view/td_view_controls.js +++ b/frontend/javascripts/oxalis/view/td_view_controls.js @@ -1,12 +1,28 @@ // @flow -import { Button } from "antd"; +import { Button, Tooltip } from "antd"; import * as React from "react"; +import { connect } from "react-redux"; +import type { OxalisState, VolumeTracing } from "oxalis/store"; import api from "oxalis/api/internal_api"; const ButtonGroup = Button.Group; -function TDViewControls() { +type Props = {| + renderIsosurfaces: boolean, + isRefreshingIsosurfaces: boolean, + volumeTracing: ?VolumeTracing, +|}; + +function TDViewControls({ renderIsosurfaces, isRefreshingIsosurfaces, volumeTracing }: Props) { + let refreshIsosurfaceTooltip = "Load Isosurface of centered cell from segmentation layer."; + if (volumeTracing != null) { + if (volumeTracing.fallbackLayer != null) { + refreshIsosurfaceTooltip = "Load Isosurface of centered cell from fallback annotation layer"; + } else { + refreshIsosurfaceTooltip = "Reload annotated Isosurfaces to newest version."; + } + } return ( + {renderIsosurfaces ? ( + +