From 88af0514103c2e66744e2f3c41efab00243a657c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 13 Aug 2020 16:55:57 +0200 Subject: [PATCH 01/23] fix typo --- .../javascripts/oxalis/model/bucket_data_handling/data_cube.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js index 4df1110cf7..d6033439a3 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js @@ -59,7 +59,7 @@ class DataCube { on: Function; off: Function; - // The cube stores the buckets in a seperate array for each zoomStep. For each + // The cube stores the buckets in a separate array for each zoomStep. For each // zoomStep the cube-array contains the boundaries and an array holding the buckets. // The bucket-arrays are initialized large enough to hold the whole cube. Thus no // expanding is necessary. bucketCount keeps track of how many buckets are currently From 76357d05452b9966154db5c3182d39af25cd0bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 13 Aug 2020 19:03:32 +0200 Subject: [PATCH 02/23] WIP: Adding volume annotation in batches to undo stack --- .../volumetracing_plane_controller.js | 16 +++--- .../model/actions/volumetracing_actions.js | 33 ++++++++++-- .../model/bucket_data_handling/bucket.js | 9 +++- .../oxalis/model/sagas/save_saga.js | 54 ++++++++++++++++--- .../oxalis/model/sagas/volumetracing_saga.js | 2 + .../byte_array_to_lz4_base64.worker.js | 6 ++- .../byte_array_to_lz4_base64_temp.worker.js | 16 ++++++ .../oxalis/workers/comlink_wrapper.js | 2 +- 8 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js diff --git a/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js index 45521aee21..05bebaa67c 100644 --- a/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js +++ b/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js @@ -20,7 +20,7 @@ import { addToLayerAction, finishEditingAction, hideBrushAction, - setContourTracingMode, + setContourTracingModeAction, cycleToolAction, copySegmentationLayerAction, inferSegmentationInViewportAction, @@ -99,9 +99,9 @@ export function getPlaneMouseControls(_planeId: OrthoView): * { if (isAutomaticBrushEnabled()) { return; } - Store.dispatch(setContourTracingMode(ContourModeEnum.DRAW)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.DRAW)); } else { - Store.dispatch(setContourTracingMode(ContourModeEnum.DRAW_OVERWRITE)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.DRAW_OVERWRITE)); } Store.dispatch(startEditingAction(calculateGlobalPos(pos), plane)); } @@ -110,7 +110,7 @@ export function getPlaneMouseControls(_planeId: OrthoView): * { leftMouseUp: () => { const tool = Utils.enforce(getVolumeTool)(Store.getState().tracing.volume); - Store.dispatch(setContourTracingMode(ContourModeEnum.IDLE)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.IDLE)); if (tool === VolumeToolEnum.TRACE || tool === VolumeToolEnum.BRUSH) { Store.dispatch(finishEditingAction()); @@ -137,9 +137,9 @@ export function getPlaneMouseControls(_planeId: OrthoView): * { if (!event.shiftKey && (tool === VolumeToolEnum.TRACE || tool === VolumeToolEnum.BRUSH)) { if (event.ctrlKey) { - Store.dispatch(setContourTracingMode(ContourModeEnum.DELETE_FROM_ANY_CELL)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.DELETE_FROM_ANY_CELL)); } else { - Store.dispatch(setContourTracingMode(ContourModeEnum.DELETE_FROM_ACTIVE_CELL)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.DELETE_FROM_ACTIVE_CELL)); } Store.dispatch(startEditingAction(calculateGlobalPos(pos), plane)); } @@ -148,11 +148,11 @@ export function getPlaneMouseControls(_planeId: OrthoView): * { rightMouseUp: () => { const tool = Utils.enforce(getVolumeTool)(Store.getState().tracing.volume); - Store.dispatch(setContourTracingMode(ContourModeEnum.IDLE)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.IDLE)); if (tool === VolumeToolEnum.TRACE || tool === VolumeToolEnum.BRUSH) { Store.dispatch(finishEditingAction()); - Store.dispatch(setContourTracingMode(ContourModeEnum.IDLE)); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.IDLE)); } }, diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index 60c0040e10..f182212e3a 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -3,7 +3,15 @@ * @flow */ import type { ServerVolumeTracing } from "admin/api_flow_types"; -import type { Vector2, Vector3, OrthoView, VolumeTool, ContourMode } from "oxalis/constants"; +import type { + Vector2, + Vector3, + Vector4, + OrthoView, + VolumeTool, + ContourMode, +} from "oxalis/constants"; +import type { BucketDataArray } from "oxalis/model/bucket_data_handling/bucket"; type InitializeVolumeTracingAction = { type: "INITIALIZE_VOLUMETRACING", @@ -20,11 +28,17 @@ export type CopySegmentationLayerAction = { type: "COPY_SEGMENTATION_LAYER", source: "previousLayer" | "nextLayer", }; +type AddBucketToUndoAction = { + type: "ADD_BUCKET_TO_UNDO", + zoomedBucketAddress: Vector4, + bucketData: BucketDataArray, +}; type UpdateDirectionAction = { type: "UPDATE_DIRECTION", centroid: Vector3 }; type ResetContourAction = { type: "RESET_CONTOUR" }; +type FinishAnnotationStrokeAction = { type: "FINISH_ANNOTATION_STROKE" }; type SetMousePositionAction = { type: "SET_MOUSE_POSITION", position: Vector2 }; type HideBrushAction = { type: "HIDE_BRUSH" }; -type SetContourTracingMode = { type: "SET_CONTOUR_TRACING_MODE", mode: ContourMode }; +type SetContourTracingModeAction = { type: "SET_CONTOUR_TRACING_MODE", mode: ContourMode }; export type InferSegmentationInViewportAction = { type: "INFER_SEGMENT_IN_VIEWPORT", position: Vector3, @@ -41,11 +55,13 @@ export type VolumeTracingAction = | CycleToolAction | UpdateDirectionAction | ResetContourAction + | FinishAnnotationStrokeAction | SetMousePositionAction | HideBrushAction | CopySegmentationLayerAction | InferSegmentationInViewportAction - | SetContourTracingMode; + | SetContourTracingModeAction + | AddBucketToUndoAction; export const VolumeTracingSaveRelevantActions = [ "CREATE_CELL", @@ -109,6 +125,10 @@ export const resetContourAction = (): ResetContourAction => ({ type: "RESET_CONTOUR", }); +export const finishAnnotationStrokeAction = (): FinishAnnotationStrokeAction => ({ + type: "FINISH_ANNOTATION_STROKE", +}); + export const setMousePositionAction = (position: Vector2): SetMousePositionAction => ({ type: "SET_MOUSE_POSITION", position, @@ -118,11 +138,16 @@ export const hideBrushAction = (): HideBrushAction => ({ type: "HIDE_BRUSH", }); -export const setContourTracingMode = (mode: ContourMode): SetContourTracingMode => ({ +export const setContourTracingModeAction = (mode: ContourMode): SetContourTracingModeAction => ({ type: "SET_CONTOUR_TRACING_MODE", mode, }); +export const addBucketToUndoAction = ( + zoomedBucketAddress: Vector4, + bucketData: BucketDataArray, +): AddBucketToUndoAction => ({ type: "ADD_BUCKET_TO_UNDO", zoomedBucketAddress, bucketData }); + export const inferSegmentationInViewportAction = ( position: Vector3, ): InferSegmentationInViewportAction => ({ diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index f9063fac39..aef1ae8796 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -19,6 +19,7 @@ import TemporalBucketManager from "oxalis/model/bucket_data_handling/temporal_bu import Constants, { type Vector4 } from "oxalis/constants"; import window from "libs/window"; import { type ElementClass } from "admin/api_flow_types"; +import { addBucketToUndoAction } from "oxalis/model/actions/volumetracing_actions"; export const BucketStateEnum = { UNREQUESTED: "UNREQUESTED", @@ -161,7 +162,11 @@ export class DataBucket { } label(labelFunc: BucketDataArray => void) { - labelFunc(this.getOrCreateData()); + const bucketData = this.getOrCreateData(); + if (!this.dirty) { + Store.dispatch(addBucketToUndoAction(this.zoomedAddress, bucketData)); + } + labelFunc(bucketData); this.dirty = true; this.throttledTriggerLabeled(); } @@ -173,7 +178,7 @@ export class DataBucket { } getData(): BucketDataArray { - const data = this.data; + const { data } = this; if (data == null) { throw new Error("Bucket.getData() called, but data does not exist (anymore)."); } diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 5c8f7e5b8f..279db2bbcc 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -14,7 +14,7 @@ import { UNDO_HISTORY_SIZE, maximumActionCountPerSave, } from "oxalis/model/sagas/save_saga_constants"; -import type { Tracing, Flycam, SaveQueueEntry } from "oxalis/store"; +import type { Tracing, SkeletonTracing, Flycam, SaveQueueEntry } from "oxalis/store"; import { type UpdateAction } from "oxalis/model/sagas/update_actions"; import { VolumeTracingSaveRelevantActions } from "oxalis/model/actions/volumetracing_actions"; import { @@ -52,12 +52,21 @@ import compactUpdateActions from "oxalis/model/helpers/compaction/compact_update import messages from "messages"; import window, { alert, document, location } from "libs/window"; import ErrorHandling from "libs/error_handling"; +import type { Vector4 } from "oxalis/constants"; +import compressStuff from "oxalis/workers/byte_array_to_lz4_base64_temp.worker"; +import { createWorker } from "oxalis/workers/comlink_wrapper"; import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; +const byteArrayToLz4Array = createWorker(compressStuff); + +type UndoBucket = { zoomedBucketAddress: Vector4, data: Uint8Array }; +type VolumeAnnotationBatch = Array; +type UndoState = { type: "skeleton" | "volume", data: SkeletonTracing | VolumeAnnotationBatch }; + export function* collectUndoStates(): Saga { - const undoStack = []; - const redoStack = []; + const undoStack: Array = []; + const redoStack: Array = []; let previousAction: ?Action = null; yield* take("INITIALIZE_SKELETONTRACING"); @@ -72,7 +81,7 @@ export function* collectUndoStates(): Saga { if (userAction) { if (curTracing !== prevTracing) { if (shouldAddToUndoStack(userAction, previousAction)) { - undoStack.push(prevTracing); + undoStack.push({ type: "skeleton", data: prevTracing }); } // Clear the redo stack when a new action is executed redoStack.splice(0); @@ -82,8 +91,8 @@ export function* collectUndoStates(): Saga { } else if (undo) { if (undoStack.length) { previousAction = null; - redoStack.push(prevTracing); - const newTracing = undoStack.pop(); + redoStack.push({ type: "skeleton", data: prevTracing }); + const newTracing = undoStack.pop().data; yield* put(setTracingAction(newTracing)); yield* put(centerActiveNodeAction()); } else { @@ -91,8 +100,8 @@ export function* collectUndoStates(): Saga { } } else if (redo) { if (redoStack.length) { - undoStack.push(prevTracing); - const newTracing = redoStack.pop(); + undoStack.push({ type: "skeleton", data: prevTracing }); + const newTracing = redoStack.pop().data; yield* put(setTracingAction(newTracing)); yield* put(centerActiveNodeAction()); } else { @@ -104,6 +113,35 @@ export function* collectUndoStates(): Saga { } } +function* addAnnotationStrokeToUndoStack(undoStack: Array) { + const bucketsOfCurrentStroke: VolumeAnnotationBatch = []; + while (true) { + const { addBucketToUndoAction, finishAnnotationStrokeAction } = yield* race({ + addBucketToUndoAction: _take("ADD_BUCKET_TO_UNDO"), + finishAnnotationStrokeAction: _take("FINISH_ANNOTATION_STROKE"), + }); + if (addBucketToUndoAction && addBucketToUndoAction.type === "ADD_BUCKET_TO_UNDO") { + const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; + const bucketDataAsByteArray = new Uint8Array( + bucketData.buffer, + bucketData.byteOffset, + bucketData.byteLength, + ); + // TODO: use fork no not block this saga + // TODO: use pure compress and decompress in a worker + const compressedBucketData = yield* call(byteArrayToLz4Array, bucketDataAsByteArray); + debugger; + if (compressedBucketData != null) { + bucketsOfCurrentStroke.push({ zoomedBucketAddress, data: compressedBucketData }); + } + } + if (finishAnnotationStrokeAction) { + break; + } + undoStack.push({ type: "volume", data: bucketsOfCurrentStroke }); + } +} + function shouldAddToUndoStack(currentUserAction: Action, previousAction: ?Action) { if (previousAction == null) { return true; diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index dffd7b364e..2597629fde 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -5,6 +5,7 @@ import { type CopySegmentationLayerAction, resetContourAction, updateDirectionAction, + finishAnnotationStrokeAction, } from "oxalis/model/actions/volumetracing_actions"; import { type Saga, @@ -253,6 +254,7 @@ export function* finishLayer( yield* put(updateDirectionAction(layer.getCentroid())); yield* put(resetContourAction()); + yield* put(finishAnnotationStrokeAction()); } export function* disallowVolumeTracingWarning(): Saga<*> { diff --git a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js index a7c4f4a535..aa507136ad 100644 --- a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js +++ b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js @@ -1,10 +1,10 @@ // @flow import Base64 from "base64-js"; import lz4 from "lz4js"; - +import {compressLz4Block} "./byte_array_to_lz4_base64_temp.worker"; import { expose } from "./comlink_wrapper"; -function compressLz4Block(data: Uint8Array): Uint8Array { +function compressLz4BlockQ(data: Uint8Array): Uint8Array { // Backend expects the frame-less version of lz4, // so we need to call lz4.compressBlock rather than compress const hashSize = 1 << 16; @@ -19,4 +19,6 @@ export function byteArrayToLz4Base64(byteArray: Uint8Array): string { return Base64.fromByteArray(compressed); } +export const compressLz4Block = expose(compressLz4BlockQ); + export default expose(byteArrayToLz4Base64); diff --git a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js new file mode 100644 index 0000000000..ee428981e7 --- /dev/null +++ b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js @@ -0,0 +1,16 @@ +// @flow +import lz4 from "lz4js"; + +import { expose } from "./comlink_wrapper"; + +export function compressLz4Block(data: Uint8Array): Uint8Array { + // Backend expects the frame-less version of lz4, + // so we need to call lz4.compressBlock rather than compress + const hashSize = 1 << 16; + const hashTable = new Uint32Array(hashSize); + const compressedBuffer = new Uint8Array(data.length); + const compressedSize = lz4.compressBlock(data, compressedBuffer, 0, data.length, hashTable); + return compressedBuffer.slice(0, compressedSize); +} + +export default expose(compressLz4Block); diff --git a/frontend/javascripts/oxalis/workers/comlink_wrapper.js b/frontend/javascripts/oxalis/workers/comlink_wrapper.js index 5e0d5af1e0..b36f0f9b52 100644 --- a/frontend/javascripts/oxalis/workers/comlink_wrapper.js +++ b/frontend/javascripts/oxalis/workers/comlink_wrapper.js @@ -47,7 +47,7 @@ export function createWorker(WorkerClass: UseCreateWorkerToUseMe): T { // $FlowIgnore return WorkerClass; } - + debugger; return wrap( // $FlowIgnore new WorkerClass(), From f2d4f0d5d2d640ea66f90230031fe892ae13cb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 14 Aug 2020 14:02:09 +0200 Subject: [PATCH 03/23] first version with undo - not everthing is undone :/ --- .../model/actions/volumetracing_actions.js | 2 + .../model/bucket_data_handling/bucket.js | 10 + .../oxalis/model/sagas/effect-generators.js | 4 + .../oxalis/model/sagas/save_saga.js | 171 ++++++++++++++---- .../oxalis/model/sagas/volumetracing_saga.js | 1 + .../byte_array_to_lz4_base64.worker.js | 5 +- .../byte_array_to_lz4_base64_temp.worker.js | 13 +- 7 files changed, 158 insertions(+), 48 deletions(-) diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index f182212e3a..fbfe19434b 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -70,6 +70,8 @@ export const VolumeTracingSaveRelevantActions = [ "ADD_USER_BOUNDING_BOXES", ]; +export const VolumeTracingUndoRelevantActions = ["START_EDITING", "COPY_SEGMENTATION_LAYER"]; + export const initializeVolumeTracingAction = ( tracing: ServerVolumeTracing, ): InitializeVolumeTracingAction => ({ diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index aef1ae8796..493f6b448f 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -186,6 +186,16 @@ export class DataBucket { return data; } + setData(newData: Uint8Array) { + const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; + this.data = new TypedArrayClass( + newData.buffer, + newData.byteOffset, + newData.byteLength / TypedArrayClass.BYTES_PER_ELEMENT, + ); + this.trigger("bucketLoaded"); + } + markAsNeeded(): void { this.accessed = true; } diff --git a/frontend/javascripts/oxalis/model/sagas/effect-generators.js b/frontend/javascripts/oxalis/model/sagas/effect-generators.js index 3c0dc13223..b89f33e6bf 100644 --- a/frontend/javascripts/oxalis/model/sagas/effect-generators.js +++ b/frontend/javascripts/oxalis/model/sagas/effect-generators.js @@ -34,6 +34,10 @@ export function* race(...args) { return yield IOEffects.race(...args); } +export function* join(...args) { + return yield IOEffects.join(...args); +} + export const _take = IOEffects.take; export const _call = IOEffects.call; export const _takeEvery = IOEffects.takeEvery; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 279db2bbcc..de2fa97e80 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -16,7 +16,10 @@ import { } from "oxalis/model/sagas/save_saga_constants"; import type { Tracing, SkeletonTracing, Flycam, SaveQueueEntry } from "oxalis/store"; import { type UpdateAction } from "oxalis/model/sagas/update_actions"; -import { VolumeTracingSaveRelevantActions } from "oxalis/model/actions/volumetracing_actions"; +import { + VolumeTracingSaveRelevantActions, + VolumeTracingUndoRelevantActions, +} from "oxalis/model/actions/volumetracing_actions"; import { _all, _delay, @@ -27,9 +30,12 @@ import { call, put, select, + join, + fork, } from "oxalis/model/sagas/effect-generators"; import { SkeletonTracingSaveRelevantActions, + SkeletonTracingAction, setTracingAction, centerActiveNodeAction, } from "oxalis/model/actions/skeletontracing_actions"; @@ -44,6 +50,7 @@ import { pushSaveQueueTransaction, setVersionNumberAction, } from "oxalis/model/actions/save_actions"; +import Model from "oxalis/model"; import Date from "libs/date"; import Request, { type RequestOptionsWithData } from "libs/request"; import Toast from "libs/toast"; @@ -55,52 +62,79 @@ import ErrorHandling from "libs/error_handling"; import type { Vector4 } from "oxalis/constants"; import compressStuff from "oxalis/workers/byte_array_to_lz4_base64_temp.worker"; import { createWorker } from "oxalis/workers/comlink_wrapper"; - +import type { BucketDataArray } from "oxalis/model/bucket_data_handling/bucket"; import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; const byteArrayToLz4Array = createWorker(compressStuff); type UndoBucket = { zoomedBucketAddress: Vector4, data: Uint8Array }; type VolumeAnnotationBatch = Array; -type UndoState = { type: "skeleton" | "volume", data: SkeletonTracing | VolumeAnnotationBatch }; +type SkeletonUndoState = { type: "skeleton", data: SkeletonTracing }; +type VolumeUndoState = { type: "volume", data: VolumeAnnotationBatch }; +type UndoState = SkeletonUndoState | VolumeUndoState; export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; let previousAction: ?Action = null; + let prevTracingMaybe: ?SkeletonTracing = null; - yield* take("INITIALIZE_SKELETONTRACING"); - let prevTracing = yield* select(state => enforceSkeletonTracing(state.tracing)); + yield* take(["INITIALIZE_SKELETONTRACING", "INITIALIZE_VOLUMETRACING"]); + prevTracingMaybe = yield* select(state => state.tracing.skeleton); while (true) { - const { userAction, undo, redo } = yield* race({ - userAction: _take(SkeletonTracingSaveRelevantActions), + const { skeletonUserAction, startingVolumeAnnotationAction, undo, redo } = yield* race({ + skeletonUserAction: _take(SkeletonTracingSaveRelevantActions), + startingVolumeAnnotationAction: _take(VolumeTracingUndoRelevantActions), undo: _take("UNDO"), redo: _take("REDO"), }); - const curTracing = yield* select(state => enforceSkeletonTracing(state.tracing)); - if (userAction) { - if (curTracing !== prevTracing) { - if (shouldAddToUndoStack(userAction, previousAction)) { - undoStack.push({ type: "skeleton", data: prevTracing }); + if (skeletonUserAction || startingVolumeAnnotationAction) { + if (skeletonUserAction && prevTracingMaybe != null) { + const skeletonUndoState = yield* call( + getSkeletonTracingToUndoState, + skeletonUserAction, + prevTracingMaybe, + previousAction, + ); + if (skeletonUndoState) { + undoStack.push(skeletonUndoState); } - // Clear the redo stack when a new action is executed - redoStack.splice(0); - if (undoStack.length > UNDO_HISTORY_SIZE) undoStack.shift(); - previousAction = userAction; + } else if (startingVolumeAnnotationAction) { + const volumeAnnotationBatch = yield* call(putAnnotatedBucketsIntoUndoBatch); + undoStack.push({ type: "volume", data: volumeAnnotationBatch }); + } + // Clear the redo stack when a new action is executed + redoStack.splice(0); + if (undoStack.length > UNDO_HISTORY_SIZE) { + undoStack.shift(); } + previousAction = skeletonUserAction; } else if (undo) { if (undoStack.length) { - previousAction = null; - redoStack.push({ type: "skeleton", data: prevTracing }); - const newTracing = undoStack.pop().data; - yield* put(setTracingAction(newTracing)); - yield* put(centerActiveNodeAction()); + const lastUndoState = undoStack[undoStack.length - 1]; + if (lastUndoState.type === "skeleton") { + previousAction = null; + redoStack.push({ type: "skeleton", data: prevTracingMaybe }); + const newTracing = undoStack.pop().data; + yield* put(setTracingAction(newTracing)); + yield* put(centerActiveNodeAction()); + } else if (lastUndoState.type === "volume") { + const allZoomedBucketAddresses = lastUndoState.data.map( + undoBucket => undoBucket.zoomedBucketAddress, + ); + const redoVolumeAnnotationState = yield* call( + getUndoStateFromBucketArray, + allZoomedBucketAddresses, + ); + redoStack.push(redoVolumeAnnotationState); + yield* call(applyVolumeUndoAnnotationBatch, lastUndoState.data); + } } else { Toast.info(messages["undo.no_undo"]); } } else if (redo) { if (redoStack.length) { - undoStack.push({ type: "skeleton", data: prevTracing }); + undoStack.push({ type: "skeleton", data: prevTracingMaybe }); const newTracing = redoStack.pop().data; yield* put(setTracingAction(newTracing)); yield* put(centerActiveNodeAction()); @@ -109,12 +143,46 @@ export function* collectUndoStates(): Saga { } } // We need the updated tracing here - prevTracing = yield* select(state => enforceSkeletonTracing(state.tracing)); + prevTracingMaybe = yield* select(state => state.tracing.skeleton); + } +} + +function* getSkeletonTracingToUndoState( + skeletonUserAction: SkeletonTracingAction, + prevTracing: SkeletonTracing, + previousAction: ?Action, +): Saga { + const curTracing = yield* select(state => enforceSkeletonTracing(state.tracing)); + if (curTracing !== prevTracing) { + if (shouldAddToUndoStack(skeletonUserAction, previousAction)) { + return { type: "skeleton", data: prevTracing }; + } + } + return null; +} + +function* compressBucketAndAddToUndoBatch( + zoomedBucketAddress: Vector4, + bucketData: BucketDataArray, + undoBatch: VolumeAnnotationBatch, +) { + const bucketDataAsByteArray = new Uint8Array( + bucketData.buffer, + bucketData.byteOffset, + bucketData.byteLength, + ); + console.log("uncompressed length", bucketDataAsByteArray.length); + const compressedBucketData = yield* call(byteArrayToLz4Array, bucketDataAsByteArray, true); + if (compressedBucketData != null) { + console.log("compressed length", compressedBucketData.length); + undoBatch.push({ zoomedBucketAddress, data: compressedBucketData }); } } -function* addAnnotationStrokeToUndoStack(undoStack: Array) { +function* putAnnotatedBucketsIntoUndoBatch(): Saga { const bucketsOfCurrentStroke: VolumeAnnotationBatch = []; + const pendingCompressions: Array> = []; + const alreadyAddedBuckets: Set = new Set(); while (true) { const { addBucketToUndoAction, finishAnnotationStrokeAction } = yield* race({ addBucketToUndoAction: _take("ADD_BUCKET_TO_UNDO"), @@ -122,24 +190,24 @@ function* addAnnotationStrokeToUndoStack(undoStack: Array) { }); if (addBucketToUndoAction && addBucketToUndoAction.type === "ADD_BUCKET_TO_UNDO") { const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; - const bucketDataAsByteArray = new Uint8Array( - bucketData.buffer, - bucketData.byteOffset, - bucketData.byteLength, - ); - // TODO: use fork no not block this saga - // TODO: use pure compress and decompress in a worker - const compressedBucketData = yield* call(byteArrayToLz4Array, bucketDataAsByteArray); - debugger; - if (compressedBucketData != null) { - bucketsOfCurrentStroke.push({ zoomedBucketAddress, data: compressedBucketData }); + if (!alreadyAddedBuckets.has(zoomedBucketAddress)) { + alreadyAddedBuckets.add(zoomedBucketAddress); + pendingCompressions.push( + yield* fork( + compressBucketAndAddToUndoBatch, + zoomedBucketAddress, + bucketData, + bucketsOfCurrentStroke, + ), + ); } } if (finishAnnotationStrokeAction) { break; } - undoStack.push({ type: "volume", data: bucketsOfCurrentStroke }); } + yield* join(pendingCompressions); + return bucketsOfCurrentStroke; } function shouldAddToUndoStack(currentUserAction: Action, previousAction: ?Action) { @@ -161,6 +229,37 @@ function shouldAddToUndoStack(currentUserAction: Action, previousAction: ?Action } } +function* getUndoStateFromBucketArray( + zoomedBucketAddresses: Array, +): Saga { + const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); + if (!segmentationLayer) { + throw new Error("Undoing a volume annotation but no volume layer exists."); + } + const { cube } = segmentationLayer; + const allCompressedBuckets: VolumeAnnotationBatch = []; + for (const address of zoomedBucketAddresses) { + const bucket = yield* call([cube, cube.getOrCreateBucket], address); + const bucketData = bucket.getData(); + yield* call(compressBucketAndAddToUndoBatch, address, bucketData, allCompressedBuckets); + } + return { type: "volume", data: allCompressedBuckets }; +} + +function* applyVolumeUndoAnnotationBatch(volumeAnnotationBatch: VolumeAnnotationBatch) { + const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); + if (!segmentationLayer) { + throw new Error("Undoing a volume annotation but no volume layer exists."); + } + const { cube } = segmentationLayer; + for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { + debugger; + const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); + const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); + bucket.setData(decompressedBucketData); + } +} + export function* pushAnnotationAsync(): Saga { yield _all([_call(pushTracingTypeAsync, "skeleton"), _call(pushTracingTypeAsync, "volume")]); } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index 2597629fde..812d634bcb 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -237,6 +237,7 @@ function* copySegmentationLayer(action: CopySegmentationLayerAction): Saga ); } } + yield* put(finishAnnotationStrokeAction()); } export function* finishLayer( diff --git a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js index aa507136ad..99be568486 100644 --- a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js +++ b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js @@ -1,10 +1,9 @@ // @flow import Base64 from "base64-js"; import lz4 from "lz4js"; -import {compressLz4Block} "./byte_array_to_lz4_base64_temp.worker"; import { expose } from "./comlink_wrapper"; -function compressLz4BlockQ(data: Uint8Array): Uint8Array { +function compressLz4Block(data: Uint8Array): Uint8Array { // Backend expects the frame-less version of lz4, // so we need to call lz4.compressBlock rather than compress const hashSize = 1 << 16; @@ -19,6 +18,4 @@ export function byteArrayToLz4Base64(byteArray: Uint8Array): string { return Base64.fromByteArray(compressed); } -export const compressLz4Block = expose(compressLz4BlockQ); - export default expose(byteArrayToLz4Base64); diff --git a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js index ee428981e7..91379d9fc8 100644 --- a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js +++ b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js @@ -3,14 +3,11 @@ import lz4 from "lz4js"; import { expose } from "./comlink_wrapper"; -export function compressLz4Block(data: Uint8Array): Uint8Array { - // Backend expects the frame-less version of lz4, - // so we need to call lz4.compressBlock rather than compress - const hashSize = 1 << 16; - const hashTable = new Uint32Array(hashSize); - const compressedBuffer = new Uint8Array(data.length); - const compressedSize = lz4.compressBlock(data, compressedBuffer, 0, data.length, hashTable); - return compressedBuffer.slice(0, compressedSize); +export function compressLz4Block(data: Uint8Array, compress: boolean): Uint8Array { + if (compress) { + return lz4.compress(data); + } + return lz4.decompress(data); } export default expose(compressLz4Block); From defa7aafe0f7ebb63f0d0d3f9dff40d53c5e0798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 17 Aug 2020 19:25:10 +0200 Subject: [PATCH 04/23] added first version of volume undo, not working correctly :/ --- .../model/bucket_data_handling/bucket.js | 4 +- .../oxalis/model/sagas/save_saga.js | 151 +++++++++--------- .../view/action-bar/tracing_actions_view.js | 27 ++-- 3 files changed, 100 insertions(+), 82 deletions(-) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index 493f6b448f..a7aa2407ca 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -164,7 +164,9 @@ export class DataBucket { label(labelFunc: BucketDataArray => void) { const bucketData = this.getOrCreateData(); if (!this.dirty) { - Store.dispatch(addBucketToUndoAction(this.zoomedAddress, bucketData)); + const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; + const dataClone = new TypedArrayClass(bucketData); + Store.dispatch(addBucketToUndoAction(this.zoomedAddress, dataClone)); } labelFunc(bucketData); this.dirty = true; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index de2fa97e80..c4a70e62d2 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -16,10 +16,7 @@ import { } from "oxalis/model/sagas/save_saga_constants"; import type { Tracing, SkeletonTracing, Flycam, SaveQueueEntry } from "oxalis/store"; import { type UpdateAction } from "oxalis/model/sagas/update_actions"; -import { - VolumeTracingSaveRelevantActions, - VolumeTracingUndoRelevantActions, -} from "oxalis/model/actions/volumetracing_actions"; +import { VolumeTracingSaveRelevantActions } from "oxalis/model/actions/volumetracing_actions"; import { _all, _delay, @@ -78,17 +75,26 @@ export function* collectUndoStates(): Saga { const redoStack: Array = []; let previousAction: ?Action = null; let prevTracingMaybe: ?SkeletonTracing = null; + let pendingCompressions: Array> = []; + let currentVolumeAnnotationBatch: VolumeAnnotationBatch = []; yield* take(["INITIALIZE_SKELETONTRACING", "INITIALIZE_VOLUMETRACING"]); prevTracingMaybe = yield* select(state => state.tracing.skeleton); while (true) { - const { skeletonUserAction, startingVolumeAnnotationAction, undo, redo } = yield* race({ + const { + skeletonUserAction, + addBucketToUndoAction, + finishAnnotationStrokeAction, + undo, + redo, + } = yield* race({ skeletonUserAction: _take(SkeletonTracingSaveRelevantActions), - startingVolumeAnnotationAction: _take(VolumeTracingUndoRelevantActions), + addBucketToUndoAction: _take("ADD_BUCKET_TO_UNDO"), + finishAnnotationStrokeAction: _take("FINISH_ANNOTATION_STROKE"), undo: _take("UNDO"), redo: _take("REDO"), }); - if (skeletonUserAction || startingVolumeAnnotationAction) { + if (skeletonUserAction || addBucketToUndoAction || finishAnnotationStrokeAction) { if (skeletonUserAction && prevTracingMaybe != null) { const skeletonUndoState = yield* call( getSkeletonTracingToUndoState, @@ -99,9 +105,22 @@ export function* collectUndoStates(): Saga { if (skeletonUndoState) { undoStack.push(skeletonUndoState); } - } else if (startingVolumeAnnotationAction) { - const volumeAnnotationBatch = yield* call(putAnnotatedBucketsIntoUndoBatch); - undoStack.push({ type: "volume", data: volumeAnnotationBatch }); + } else if (addBucketToUndoAction) { + const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; + pendingCompressions.push( + yield* fork( + compressBucketAndAddToUndoBatch, + zoomedBucketAddress, + bucketData, + currentVolumeAnnotationBatch, + ), + ); + } else if (finishAnnotationStrokeAction) { + yield* join([...pendingCompressions]); + console.log("adding batch of ", currentVolumeAnnotationBatch.length, " buckets"); + undoStack.push({ type: "volume", data: currentVolumeAnnotationBatch }); + currentVolumeAnnotationBatch = []; + pendingCompressions = []; } // Clear the redo stack when a new action is executed redoStack.splice(0); @@ -110,37 +129,27 @@ export function* collectUndoStates(): Saga { } previousAction = skeletonUserAction; } else if (undo) { - if (undoStack.length) { - const lastUndoState = undoStack[undoStack.length - 1]; - if (lastUndoState.type === "skeleton") { - previousAction = null; - redoStack.push({ type: "skeleton", data: prevTracingMaybe }); - const newTracing = undoStack.pop().data; - yield* put(setTracingAction(newTracing)); - yield* put(centerActiveNodeAction()); - } else if (lastUndoState.type === "volume") { - const allZoomedBucketAddresses = lastUndoState.data.map( - undoBucket => undoBucket.zoomedBucketAddress, - ); - const redoVolumeAnnotationState = yield* call( - getUndoStateFromBucketArray, - allZoomedBucketAddresses, - ); - redoStack.push(redoVolumeAnnotationState); - yield* call(applyVolumeUndoAnnotationBatch, lastUndoState.data); - } - } else { - Toast.info(messages["undo.no_undo"]); + if (undoStack.length && undoStack[undoStack.length - 1].type === "skeleton") { + previousAction = null; } + yield* call( + applyStateOfStack, + undoStack, + redoStack, + prevTracingMaybe, + messages["undo.no_undo"], + ); } else if (redo) { - if (redoStack.length) { - undoStack.push({ type: "skeleton", data: prevTracingMaybe }); - const newTracing = redoStack.pop().data; - yield* put(setTracingAction(newTracing)); - yield* put(centerActiveNodeAction()); - } else { - Toast.info(messages["undo.no_redo"]); + if (redoStack && redoStack[redoStack.length - 1].type === "skeleton") { + previousAction = null; } + yield* call( + applyStateOfStack, + redoStack, + undoStack, + prevTracingMaybe, + messages["undo.no_redo"], + ); } // We need the updated tracing here prevTracingMaybe = yield* select(state => state.tracing.skeleton); @@ -171,45 +180,12 @@ function* compressBucketAndAddToUndoBatch( bucketData.byteOffset, bucketData.byteLength, ); - console.log("uncompressed length", bucketDataAsByteArray.length); const compressedBucketData = yield* call(byteArrayToLz4Array, bucketDataAsByteArray, true); if (compressedBucketData != null) { - console.log("compressed length", compressedBucketData.length); undoBatch.push({ zoomedBucketAddress, data: compressedBucketData }); } } -function* putAnnotatedBucketsIntoUndoBatch(): Saga { - const bucketsOfCurrentStroke: VolumeAnnotationBatch = []; - const pendingCompressions: Array> = []; - const alreadyAddedBuckets: Set = new Set(); - while (true) { - const { addBucketToUndoAction, finishAnnotationStrokeAction } = yield* race({ - addBucketToUndoAction: _take("ADD_BUCKET_TO_UNDO"), - finishAnnotationStrokeAction: _take("FINISH_ANNOTATION_STROKE"), - }); - if (addBucketToUndoAction && addBucketToUndoAction.type === "ADD_BUCKET_TO_UNDO") { - const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; - if (!alreadyAddedBuckets.has(zoomedBucketAddress)) { - alreadyAddedBuckets.add(zoomedBucketAddress); - pendingCompressions.push( - yield* fork( - compressBucketAndAddToUndoBatch, - zoomedBucketAddress, - bucketData, - bucketsOfCurrentStroke, - ), - ); - } - } - if (finishAnnotationStrokeAction) { - break; - } - } - yield* join(pendingCompressions); - return bucketsOfCurrentStroke; -} - function shouldAddToUndoStack(currentUserAction: Action, previousAction: ?Action) { if (previousAction == null) { return true; @@ -229,6 +205,38 @@ function shouldAddToUndoStack(currentUserAction: Action, previousAction: ?Action } } +function* applyStateOfStack( + sourceStack: Array, + stackToPushTo: Array, + prevTracingMaybe: ?SkeletonTracing, + warningMessage: string, +) { + if (sourceStack.length <= 0) { + Toast.info(warningMessage); + return; + } + const stateToRestore = sourceStack.pop(); + if (stateToRestore.type === "skeleton") { + stackToPushTo.push({ type: "skeleton", data: prevTracingMaybe }); + const newTracing = stateToRestore.data; + yield* put(setTracingAction(newTracing)); + yield* put(centerActiveNodeAction()); + } else if (stateToRestore.type === "volume") { + const allZoomedBucketAddresses = stateToRestore.data.map( + undoBucket => undoBucket.zoomedBucketAddress, + ); + const currentVolumeState = yield* call( + getUndoStateFromBucketArray, + allZoomedBucketAddresses, + stackToPushTo, + ); + console.log("restoring", allZoomedBucketAddresses); + stackToPushTo.push(currentVolumeState); + const volumeStateToRestore = stateToRestore.data; + yield* call(applyVolumeUndoAnnotationBatch, volumeStateToRestore); + } +} + function* getUndoStateFromBucketArray( zoomedBucketAddresses: Array, ): Saga { @@ -253,7 +261,6 @@ function* applyVolumeUndoAnnotationBatch(volumeAnnotationBatch: VolumeAnnotation } const { cube } = segmentationLayer; for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { - debugger; const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); bucket.setData(decompressedBucketData); diff --git a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js index c43b8319c5..12e3fb23ef 100644 --- a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js @@ -325,12 +325,20 @@ class TracingActionsView extends React.PureComponent { render() { const { viewMode } = Store.getState().temporaryConfiguration; const isSkeletonMode = Constants.MODES_SKELETON.includes(viewMode); - const archiveButtonText = this.props.task ? "Finish and go to Dashboard" : "Archive"; - const { restrictions } = this.props; + const { + hasTracing, + restrictions, + task, + annotationType, + annotationId, + activeUser, + layoutMenu, + } = this.props; + const archiveButtonText = task ? "Finish and go to Dashboard" : "Archive"; const saveButton = restrictions.allowUpdate ? [ - isSkeletonMode + hasTracing ? [ { ]; const finishAndNextTaskButton = - restrictions.allowFinish && this.props.task ? ( + restrictions.allowFinish && task ? ( { key="share-modal" isVisible={this.state.isShareModalOpen} onOk={this.handleShareClose} - annotationType={this.props.annotationType} - annotationId={this.props.annotationId} + annotationType={annotationType} + annotationId={annotationId} />, ); elements.push( @@ -449,7 +457,7 @@ class TracingActionsView extends React.PureComponent { />, ); - if (isSkeletonMode && this.props.activeUser != null) { + if (isSkeletonMode && activeUser != null) { elements.push( @@ -472,9 +480,9 @@ class TracingActionsView extends React.PureComponent { , ); - elements.push(this.props.layoutMenu); + elements.push(layoutMenu); - if (restrictions.allowSave && !this.props.task) { + if (restrictions.allowSave && !task) { elements.push( @@ -509,6 +517,7 @@ function mapStateToProps(state: OxalisState): StateProps { restrictions: state.tracing.restrictions, task: state.task, activeUser: state.activeUser, + hasTracing: (state.tracing.skeleton || state.tracing.volume) != null, }; } From 94d3b4aafa543f5e5cabf81d0b7fdc6b93d2cd89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 13:51:22 +0200 Subject: [PATCH 05/23] I think undo works now --- .../model/bucket_data_handling/bucket.js | 11 +++- .../oxalis/model/sagas/save_saga.js | 56 ++++++++++++++----- .../oxalis/workers/comlink_wrapper.js | 2 +- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index a7aa2407ca..acc1727de8 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -91,7 +91,8 @@ export const NULL_BUCKET_OUT_OF_BB = new NullBucket(true); // we have to define it here. // eslint-disable-next-line no-use-before-define export type Bucket = DataBucket | NullBucket; - +window.bucketAddressSet = new Set(); +window.setBucketAddressSet = new Set(); export class DataBucket { type: "data" = "data"; elementClass: ElementClass; @@ -163,11 +164,12 @@ export class DataBucket { label(labelFunc: BucketDataArray => void) { const bucketData = this.getOrCreateData(); - if (!this.dirty) { + if (!window.bucketAddressSet.has(this.zoomedAddress)) { const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; const dataClone = new TypedArrayClass(bucketData); Store.dispatch(addBucketToUndoAction(this.zoomedAddress, dataClone)); } + window.bucketAddressSet.add(this.zoomedAddress); labelFunc(bucketData); this.dirty = true; this.throttledTriggerLabeled(); @@ -189,13 +191,16 @@ export class DataBucket { } setData(newData: Uint8Array) { + window.setBucketAddressSet.add(this.zoomedAddress); + console.log("setting data at", this.zoomedAddress); const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; this.data = new TypedArrayClass( newData.buffer, newData.byteOffset, newData.byteLength / TypedArrayClass.BYTES_PER_ELEMENT, ); - this.trigger("bucketLoaded"); + // this.dirty = true; + this.trigger("bucketLabeled"); } markAsNeeded(): void { diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index c4a70e62d2..5457edfc96 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -107,6 +107,7 @@ export function* collectUndoStates(): Saga { } } else if (addBucketToUndoAction) { const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; + console.log("adding bucket at address", zoomedBucketAddress); pendingCompressions.push( yield* fork( compressBucketAndAddToUndoBatch, @@ -117,6 +118,7 @@ export function* collectUndoStates(): Saga { ); } else if (finishAnnotationStrokeAction) { yield* join([...pendingCompressions]); + window.bucketAddressSet.clear(); console.log("adding batch of ", currentVolumeAnnotationBatch.length, " buckets"); undoStack.push({ type: "volume", data: currentVolumeAnnotationBatch }); currentVolumeAnnotationBatch = []; @@ -129,6 +131,9 @@ export function* collectUndoStates(): Saga { } previousAction = skeletonUserAction; } else if (undo) { + console.log("annotated addresses"); + console.log(...Array.from(window.bucketAddressSet.keys())); + window.bucketAddressSet.clear(); if (undoStack.length && undoStack[undoStack.length - 1].type === "skeleton") { previousAction = null; } @@ -139,6 +144,13 @@ export function* collectUndoStates(): Saga { prevTracingMaybe, messages["undo.no_undo"], ); + console.log("updated addresses"); + console.log(...Array.from(window.setBucketAddressSet.keys())); + console.log("limiter"); + console.log("limiter"); + console.log("limiter"); + console.log("limiter"); + window.setBucketAddressSet.clear(); } else if (redo) { if (redoStack && redoStack[redoStack.length - 1].type === "skeleton") { previousAction = null; @@ -222,36 +234,46 @@ function* applyStateOfStack( yield* put(setTracingAction(newTracing)); yield* put(centerActiveNodeAction()); } else if (stateToRestore.type === "volume") { - const allZoomedBucketAddresses = stateToRestore.data.map( - undoBucket => undoBucket.zoomedBucketAddress, - ); + console.log("updating", stateToRestore.data.length, "many buckets"); const currentVolumeState = yield* call( getUndoStateFromBucketArray, - allZoomedBucketAddresses, + stateToRestore.data, stackToPushTo, ); - console.log("restoring", allZoomedBucketAddresses); stackToPushTo.push(currentVolumeState); - const volumeStateToRestore = stateToRestore.data; - yield* call(applyVolumeUndoAnnotationBatch, volumeStateToRestore); + // const volumeStateToRestore = stateToRestore.data; + // yield* call(applyVolumeUndoAnnotationBatch, volumeStateToRestore); } } function* getUndoStateFromBucketArray( - zoomedBucketAddresses: Array, + volumeAnnotationBatch: VolumeAnnotationBatch, ): Saga { const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); if (!segmentationLayer) { throw new Error("Undoing a volume annotation but no volume layer exists."); } const { cube } = segmentationLayer; - const allCompressedBuckets: VolumeAnnotationBatch = []; - for (const address of zoomedBucketAddresses) { - const bucket = yield* call([cube, cube.getOrCreateBucket], address); + const allCompressedBucketsOfCurrentState: VolumeAnnotationBatch = []; + let counter = 0; + for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { + ++counter; + const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); const bucketData = bucket.getData(); - yield* call(compressBucketAndAddToUndoBatch, address, bucketData, allCompressedBuckets); + yield* call( + compressBucketAndAddToUndoBatch, + zoomedBucketAddress, + bucketData, + allCompressedBucketsOfCurrentState, + ); + const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); + yield* call([bucket, bucket.setData], decompressedBucketData); } - return { type: "volume", data: allCompressedBuckets }; + console.log("updated", counter, "may buckets for undo"); + return { + type: "volume", + data: allCompressedBucketsOfCurrentState, + }; } function* applyVolumeUndoAnnotationBatch(volumeAnnotationBatch: VolumeAnnotationBatch) { @@ -262,7 +284,15 @@ function* applyVolumeUndoAnnotationBatch(volumeAnnotationBatch: VolumeAnnotation const { cube } = segmentationLayer; for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); + if (bucket.type === "null") { + console.log("got null bucket at", zoomedBucketAddress); + } const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); + let counter = 0; + for (let i = 0; i < decompressedBucketData; ++i) { + counter += decompressedBucketData[i]; + } + console.log("bucket has counter", counter); bucket.setData(decompressedBucketData); } } diff --git a/frontend/javascripts/oxalis/workers/comlink_wrapper.js b/frontend/javascripts/oxalis/workers/comlink_wrapper.js index b36f0f9b52..5e0d5af1e0 100644 --- a/frontend/javascripts/oxalis/workers/comlink_wrapper.js +++ b/frontend/javascripts/oxalis/workers/comlink_wrapper.js @@ -47,7 +47,7 @@ export function createWorker(WorkerClass: UseCreateWorkerToUseMe): T { // $FlowIgnore return WorkerClass; } - debugger; + return wrap( // $FlowIgnore new WorkerClass(), From 72975e828e7fdee3cbf2471044751b4e341af803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 14:13:42 +0200 Subject: [PATCH 06/23] clean up --- .../model/bucket_data_handling/bucket.js | 12 ++--- .../oxalis/model/sagas/save_saga.js | 53 ++++--------------- 2 files changed, 14 insertions(+), 51 deletions(-) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index acc1727de8..8bcfae59c0 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -29,6 +29,7 @@ export const BucketStateEnum = { }; export type BucketStateEnumType = $Keys; export type BucketDataArray = Uint8Array | Uint16Array | Uint32Array | Float32Array; +export const bucketAlreadyInUndoState = new Set(); export const bucketDebuggingFlags = { // For visualizing buckets which are passed to the GPU @@ -91,8 +92,7 @@ export const NULL_BUCKET_OUT_OF_BB = new NullBucket(true); // we have to define it here. // eslint-disable-next-line no-use-before-define export type Bucket = DataBucket | NullBucket; -window.bucketAddressSet = new Set(); -window.setBucketAddressSet = new Set(); + export class DataBucket { type: "data" = "data"; elementClass: ElementClass; @@ -164,12 +164,12 @@ export class DataBucket { label(labelFunc: BucketDataArray => void) { const bucketData = this.getOrCreateData(); - if (!window.bucketAddressSet.has(this.zoomedAddress)) { + if (!bucketAlreadyInUndoState.has(this.zoomedAddress)) { const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; const dataClone = new TypedArrayClass(bucketData); Store.dispatch(addBucketToUndoAction(this.zoomedAddress, dataClone)); } - window.bucketAddressSet.add(this.zoomedAddress); + bucketAlreadyInUndoState.add(this.zoomedAddress); labelFunc(bucketData); this.dirty = true; this.throttledTriggerLabeled(); @@ -191,15 +191,13 @@ export class DataBucket { } setData(newData: Uint8Array) { - window.setBucketAddressSet.add(this.zoomedAddress); - console.log("setting data at", this.zoomedAddress); const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; this.data = new TypedArrayClass( newData.buffer, newData.byteOffset, newData.byteLength / TypedArrayClass.BYTES_PER_ELEMENT, ); - // this.dirty = true; + this.dirty = true; this.trigger("bucketLabeled"); } diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 5457edfc96..ce5aa5c849 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -59,7 +59,10 @@ import ErrorHandling from "libs/error_handling"; import type { Vector4 } from "oxalis/constants"; import compressStuff from "oxalis/workers/byte_array_to_lz4_base64_temp.worker"; import { createWorker } from "oxalis/workers/comlink_wrapper"; -import type { BucketDataArray } from "oxalis/model/bucket_data_handling/bucket"; +import { + bucketAlreadyInUndoState, + type BucketDataArray, +} from "oxalis/model/bucket_data_handling/bucket"; import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; const byteArrayToLz4Array = createWorker(compressStuff); @@ -107,7 +110,6 @@ export function* collectUndoStates(): Saga { } } else if (addBucketToUndoAction) { const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; - console.log("adding bucket at address", zoomedBucketAddress); pendingCompressions.push( yield* fork( compressBucketAndAddToUndoBatch, @@ -118,8 +120,7 @@ export function* collectUndoStates(): Saga { ); } else if (finishAnnotationStrokeAction) { yield* join([...pendingCompressions]); - window.bucketAddressSet.clear(); - console.log("adding batch of ", currentVolumeAnnotationBatch.length, " buckets"); + bucketAlreadyInUndoState.clear(); undoStack.push({ type: "volume", data: currentVolumeAnnotationBatch }); currentVolumeAnnotationBatch = []; pendingCompressions = []; @@ -131,9 +132,6 @@ export function* collectUndoStates(): Saga { } previousAction = skeletonUserAction; } else if (undo) { - console.log("annotated addresses"); - console.log(...Array.from(window.bucketAddressSet.keys())); - window.bucketAddressSet.clear(); if (undoStack.length && undoStack[undoStack.length - 1].type === "skeleton") { previousAction = null; } @@ -144,13 +142,6 @@ export function* collectUndoStates(): Saga { prevTracingMaybe, messages["undo.no_undo"], ); - console.log("updated addresses"); - console.log(...Array.from(window.setBucketAddressSet.keys())); - console.log("limiter"); - console.log("limiter"); - console.log("limiter"); - console.log("limiter"); - window.setBucketAddressSet.clear(); } else if (redo) { if (redoStack && redoStack[redoStack.length - 1].type === "skeleton") { previousAction = null; @@ -234,19 +225,17 @@ function* applyStateOfStack( yield* put(setTracingAction(newTracing)); yield* put(centerActiveNodeAction()); } else if (stateToRestore.type === "volume") { - console.log("updating", stateToRestore.data.length, "many buckets"); + const volumeBatchToApply = stateToRestore.data; const currentVolumeState = yield* call( - getUndoStateFromBucketArray, - stateToRestore.data, + applyVolumeAnnotationBAtchAndGetMatchingUndo, + volumeBatchToApply, stackToPushTo, ); stackToPushTo.push(currentVolumeState); - // const volumeStateToRestore = stateToRestore.data; - // yield* call(applyVolumeUndoAnnotationBatch, volumeStateToRestore); } } -function* getUndoStateFromBucketArray( +function* applyVolumeAnnotationBAtchAndGetMatchingUndo( volumeAnnotationBatch: VolumeAnnotationBatch, ): Saga { const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); @@ -255,9 +244,7 @@ function* getUndoStateFromBucketArray( } const { cube } = segmentationLayer; const allCompressedBucketsOfCurrentState: VolumeAnnotationBatch = []; - let counter = 0; for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { - ++counter; const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); const bucketData = bucket.getData(); yield* call( @@ -269,34 +256,12 @@ function* getUndoStateFromBucketArray( const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); yield* call([bucket, bucket.setData], decompressedBucketData); } - console.log("updated", counter, "may buckets for undo"); return { type: "volume", data: allCompressedBucketsOfCurrentState, }; } -function* applyVolumeUndoAnnotationBatch(volumeAnnotationBatch: VolumeAnnotationBatch) { - const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); - if (!segmentationLayer) { - throw new Error("Undoing a volume annotation but no volume layer exists."); - } - const { cube } = segmentationLayer; - for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { - const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); - if (bucket.type === "null") { - console.log("got null bucket at", zoomedBucketAddress); - } - const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); - let counter = 0; - for (let i = 0; i < decompressedBucketData; ++i) { - counter += decompressedBucketData[i]; - } - console.log("bucket has counter", counter); - bucket.setData(decompressedBucketData); - } -} - export function* pushAnnotationAsync(): Saga { yield _all([_call(pushTracingTypeAsync, "skeleton"), _call(pushTracingTypeAsync, "volume")]); } From d5b6c65fe56cef75d4e3cfb5ad42b7adac95835b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 14:19:41 +0200 Subject: [PATCH 07/23] small fix --- frontend/javascripts/oxalis/model/sagas/save_saga.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index ce5aa5c849..375853ba3d 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -125,7 +125,7 @@ export function* collectUndoStates(): Saga { currentVolumeAnnotationBatch = []; pendingCompressions = []; } - // Clear the redo stack when a new action is executed + // Clear the redo stack when a new action is executed. redoStack.splice(0); if (undoStack.length > UNDO_HISTORY_SIZE) { undoStack.shift(); @@ -143,7 +143,7 @@ export function* collectUndoStates(): Saga { messages["undo.no_undo"], ); } else if (redo) { - if (redoStack && redoStack[redoStack.length - 1].type === "skeleton") { + if (redoStack.length && redoStack[redoStack.length - 1].type === "skeleton") { previousAction = null; } yield* call( From da7f7fda75a704b41631df078387701d5d2c624e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 15:11:52 +0200 Subject: [PATCH 08/23] fix tests and adjust naming of worker --- frontend/javascripts/oxalis/model/sagas/save_saga.js | 4 ++-- ...4_temp.worker.js => byte_array_lz4_compression.worker.js} | 2 +- .../oxalis/workers/byte_array_to_lz4_base64.worker.js | 1 + frontend/javascripts/test/sagas/volumetracing_saga.spec.js | 5 +++-- 4 files changed, 7 insertions(+), 5 deletions(-) rename frontend/javascripts/oxalis/workers/{byte_array_to_lz4_base64_temp.worker.js => byte_array_lz4_compression.worker.js} (73%) diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 375853ba3d..b4cba95c3b 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -57,7 +57,7 @@ import messages from "messages"; import window, { alert, document, location } from "libs/window"; import ErrorHandling from "libs/error_handling"; import type { Vector4 } from "oxalis/constants"; -import compressStuff from "oxalis/workers/byte_array_to_lz4_base64_temp.worker"; +import compressLz4Block from "oxalis/workers/byte_array_lz4_compression.worker"; import { createWorker } from "oxalis/workers/comlink_wrapper"; import { bucketAlreadyInUndoState, @@ -65,7 +65,7 @@ import { } from "oxalis/model/bucket_data_handling/bucket"; import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; -const byteArrayToLz4Array = createWorker(compressStuff); +const byteArrayToLz4Array = createWorker(compressLz4Block); type UndoBucket = { zoomedBucketAddress: Vector4, data: Uint8Array }; type VolumeAnnotationBatch = Array; diff --git a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js b/frontend/javascripts/oxalis/workers/byte_array_lz4_compression.worker.js similarity index 73% rename from frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js rename to frontend/javascripts/oxalis/workers/byte_array_lz4_compression.worker.js index 91379d9fc8..ddf47b9624 100644 --- a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64_temp.worker.js +++ b/frontend/javascripts/oxalis/workers/byte_array_lz4_compression.worker.js @@ -3,7 +3,7 @@ import lz4 from "lz4js"; import { expose } from "./comlink_wrapper"; -export function compressLz4Block(data: Uint8Array, compress: boolean): Uint8Array { +function compressLz4Block(data: Uint8Array, compress: boolean): Uint8Array { if (compress) { return lz4.compress(data); } diff --git a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js index 99be568486..a7c4f4a535 100644 --- a/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js +++ b/frontend/javascripts/oxalis/workers/byte_array_to_lz4_base64.worker.js @@ -1,6 +1,7 @@ // @flow import Base64 from "base64-js"; import lz4 from "lz4js"; + import { expose } from "./comlink_wrapper"; function compressLz4Block(data: Uint8Array): Uint8Array { diff --git a/frontend/javascripts/test/sagas/volumetracing_saga.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga.spec.js index ae93d138d9..8dab780e68 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga.spec.js @@ -67,6 +67,7 @@ const startEditingAction = VolumeTracingActions.startEditingAction([0, 0, 0], Or const addToLayerActionFn = VolumeTracingActions.addToLayerAction; const finishEditingAction = VolumeTracingActions.finishEditingAction(); const resetContourAction = VolumeTracingActions.resetContourAction(); +const finishAnnotationStrokeAction = VolumeTracingActions.finishAnnotationStrokeAction(); test("VolumeTracingSaga shouldn't do anything if unchanged (saga test)", t => { const saga = saveTracingTypeAsync("volume"); @@ -190,7 +191,7 @@ test("finishLayer saga should emit resetContourAction and then be done (saga tes const saga = finishLayer(mockedVolumeLayer, VolumeToolEnum.TRACE); saga.next(); saga.next(); - const iterator = saga.next(); - expectValueDeepEqual(t, iterator, put(resetContourAction)); + expectValueDeepEqual(t, saga.next(), put(resetContourAction)); + expectValueDeepEqual(t, saga.next(), put(finishAnnotationStrokeAction)); t.true(saga.next().done); }); From 5182358b20517bd2bddde6b0623e1e687657079d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 15:28:58 +0200 Subject: [PATCH 09/23] add comment --- .../javascripts/oxalis/model/bucket_data_handling/bucket.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index 8bcfae59c0..ff7f6818c3 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -29,6 +29,8 @@ export const BucketStateEnum = { }; export type BucketStateEnumType = $Keys; export type BucketDataArray = Uint8Array | Uint16Array | Uint32Array | Float32Array; +// This set saves whether a bucket is already added to the current undo volume batch +// and gets cleared by the save saga after a annotation step has finished. export const bucketAlreadyInUndoState = new Set(); export const bucketDebuggingFlags = { From e5ebdc853fb4742d64213112aafc4f72ec642c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 15:31:40 +0200 Subject: [PATCH 10/23] add type annotation --- .../javascripts/oxalis/view/action-bar/tracing_actions_view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js index 12e3fb23ef..25877a91d9 100644 --- a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js @@ -43,6 +43,7 @@ type StateProps = {| restrictions: RestrictionsAndSettings, task: ?Task, activeUser: ?APIUser, + hasTracing: boolean, |}; type Props = {| ...OwnProps, ...StateProps |}; From 30a6ba8c943721e034a423b19cb3d9259154e9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 18 Aug 2020 19:29:50 +0200 Subject: [PATCH 11/23] fix flow --- .../oxalis/model/actions/save_actions.js | 4 +- .../model/actions/volumetracing_actions.js | 4 +- .../model/bucket_data_handling/bucket.js | 2 +- .../model/sagas/effect-generators.js.flow | 560 ++++++++++++++---- .../oxalis/model/sagas/save_saga.js | 63 +- 5 files changed, 482 insertions(+), 151 deletions(-) diff --git a/frontend/javascripts/oxalis/model/actions/save_actions.js b/frontend/javascripts/oxalis/model/actions/save_actions.js index ad481b37e0..054ed167e4 100644 --- a/frontend/javascripts/oxalis/model/actions/save_actions.js +++ b/frontend/javascripts/oxalis/model/actions/save_actions.js @@ -29,8 +29,8 @@ type SetVersionNumberAction = { version: number, tracingType: Tracing, }; -type UndoAction = { type: "UNDO" }; -type RedoAction = { type: "REDO" }; +export type UndoAction = { type: "UNDO" }; +export type RedoAction = { type: "REDO" }; type DisableSavingAction = { type: "DISABLE_SAVING" }; export type SaveAction = | PushSaveQueueTransaction diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index fbfe19434b..b2d28bb215 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -28,14 +28,14 @@ export type CopySegmentationLayerAction = { type: "COPY_SEGMENTATION_LAYER", source: "previousLayer" | "nextLayer", }; -type AddBucketToUndoAction = { +export type AddBucketToUndoAction = { type: "ADD_BUCKET_TO_UNDO", zoomedBucketAddress: Vector4, bucketData: BucketDataArray, }; type UpdateDirectionAction = { type: "UPDATE_DIRECTION", centroid: Vector3 }; type ResetContourAction = { type: "RESET_CONTOUR" }; -type FinishAnnotationStrokeAction = { type: "FINISH_ANNOTATION_STROKE" }; +export type FinishAnnotationStrokeAction = { type: "FINISH_ANNOTATION_STROKE" }; type SetMousePositionAction = { type: "SET_MOUSE_POSITION", position: Vector2 }; type HideBrushAction = { type: "HIDE_BRUSH" }; type SetContourTracingModeAction = { type: "SET_CONTOUR_TRACING_MODE", mode: ContourMode }; diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index ff7f6818c3..d93bb97e35 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -31,7 +31,7 @@ export type BucketStateEnumType = $Keys; export type BucketDataArray = Uint8Array | Uint16Array | Uint32Array | Float32Array; // This set saves whether a bucket is already added to the current undo volume batch // and gets cleared by the save saga after a annotation step has finished. -export const bucketAlreadyInUndoState = new Set(); +export const bucketAlreadyInUndoState: Set = new Set(); export const bucketDebuggingFlags = { // For visualizing buckets which are passed to the GPU diff --git a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow index 7a5538dcca..94a03e0ee3 100644 --- a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow +++ b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow @@ -7,8 +7,8 @@ import { type Task, type Saga as _Saga, channel, - effects -} from 'redux-saga'; + effects, +} from "redux-saga"; import { all as typedAll, race as typedRace, @@ -19,7 +19,7 @@ import { takeLeading as typedTakeLeading, throttle as typedThrottle, cancel as typedCancel, -} from 'redux-saga/effects'; +} from "redux-saga/effects"; import type { Action } from "oxalis/model/actions/actions"; import type { OxalisState } from "oxalis/store"; @@ -30,150 +30,460 @@ declare type Context = Object; // won't be noticed count) // declare type FnSpread = (...args: Array) => Promise | R; -declare type Fn0 = () => Promise | Generator<*,R,*> | R; -declare type Fn1 = (t1: T1) => Promise | Generator<*,R,*> | R; -declare type Fn2 = (t1: T1, t2: T2) => Promise | Generator<*,R,*> | R; -declare type Fn3 = (t1: T1, t2: T2, t3: T3) => Promise | Generator<*,R,*> | R; -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 Fn0 = () => Promise | Generator<*, R, *> | R; +declare type Fn1 = (t1: T1) => Promise | Generator<*, R, *> | R; +declare type Fn2 = (t1: T1, t2: T2) => Promise | Generator<*, R, *> | R; +declare type Fn3 = (t1: T1, t2: T2, t3: T3) => Promise | Generator<*, R, *> | R; +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; /* ------------------ SELECT Stuff ------------------ */ // declare type SelectFnSpread = (state: OxalisState, ...args: Array) => R; -declare type SelectFn0 = ((state: OxalisState) => R); +declare type SelectFn0 = (state: OxalisState) => R; declare type SelectFn1 = (state: OxalisState, t1: T1) => R; declare type SelectFn2 = (state: OxalisState, t1: T1, t2: T2) => R; declare type SelectFn3 = (state: OxalisState, t1: T1, t2: T2, t3: T3) => R; -declare type SelectFn4 = (state: OxalisState, t1: T1, t2: T2, t3: T3, t4: T4) => R; -declare type SelectFn5 = (state: OxalisState, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R; -declare type SelectFn6 = (state: OxalisState, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => R; - -declare type SelectFn = - & (>(selector: Fn, ...rest: Array) => Generator<*, R, *>) - & (>(selector: Fn, t1: T1, ...rest: Array) => Generator<*, R, *>) - & (>(selector: Fn, t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) - & (>(selector: Fn, t1: T1, t2: T2, t3: T3, ...rest: Array) => Generator<*, R, *>) - & (>(selector: Fn, t1: T1, t2: T2, t3: T3, t4: T4, ...rest: Array) => Generator<*, R, *>) - & (>(selector: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, ...rest: Array) => Generator<*, R, *>) - & (>(selector: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, R, *>) - // & (>(selector: Fn, t1: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...rest: Array) => Generator<*, R, *>) +declare type SelectFn4 = ( + state: OxalisState, + t1: T1, + t2: T2, + t3: T3, + t4: T4, +) => R; +declare type SelectFn5 = ( + state: OxalisState, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, +) => R; +declare type SelectFn6 = ( + state: OxalisState, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, +) => R; + +declare type SelectFn = (>( + selector: Fn, + ...rest: Array +) => Generator<*, R, *>) & + (>( + selector: Fn, + t1: T1, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + selector: Fn, + t1: T1, + t2: T2, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + selector: Fn, + t1: T1, + t2: T2, + t3: T3, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + selector: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + selector: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + selector: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + ...rest: Array + ) => Generator<*, R, *>); +// & (>(selector: Fn, t1: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...rest: Array) => Generator<*, R, *>) /* ------------------ CALL Stuff ------------------ */ -declare type ContextCallFn = - & (>(cfn: [C, Fn], ...rest: Array) => Generator<*, R, *>) - & (>(cfn: [C, Fn], t1: T1, ...rest: Array) => Generator<*, R, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) - & (>(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: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); - -declare type CallFn = - & ContextCallFn - & (>(fn: Fn, t1: T1) => Generator<*, R, *>) - & (>(fn: Fn) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, t2: T2) => Generator<*, R, *>) - & (>(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, ...args: Array) => Generator<*, R, *>); - - /* ------------------ CPS Stuff ------------------ */ - - declare type CallbackFn = (error: any, ret: R) => void; - - // declare type CpsFnSpread = (cb: CallbackFn, ...args: Array) => any; - declare type CpsFn0 = (cb: CallbackFn) => any; - declare type CpsFn1 = (t1: T1, CallbackFn) => any; - declare type CpsFn2 = (t1: T1, t2: T2, cb: CallbackFn) => any; - declare type CpsFn3 = (t1: T1, t2: T2, t3: T3, cb: CallbackFn) => any; - declare type CpsFn4 = (t1: T1, t2: T2, t3: T3, t4: T4, cb: CallbackFn) => any; - declare type CpsFn5 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, cb: CallbackFn) => any; - declare type CpsFn6 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, cb: CallbackFn) => any; - - - declare type ContextCpsFn = - & (>(cfn: [C, Fn], ...rest: Array) => Generator<*, R, *>) - & (>(cfn: [C, Fn], t1: T1, ...rest: Array) => Generator<*, R, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) - & (>(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: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); - - declare type CpsFn = - & ContextCpsFn - & (>(fn: Fn, ...rest: Array) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, ...rest: Array) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, ...rest: Array) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, ...rest: Array) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, ...rest: Array) => Generator<*, R, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, R, *>) - // & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); - - - /* ------------------ FORK Stuff ------------------ */ - - declare type ContextForkFn = - & (>(cfn: [C, Fn], ...rest: Array) => Generator<*, Task, *>) - & (>(cfn: [C, Fn], t1: T1, ...rest: Array) => Generator<*, Task, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, ...rest: Array) => Generator<*, Task, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, ...rest: Array) => Generator<*, Task, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, ...rest: Array) => Generator<*, Task, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, ...rest: Array) => Generator<*, Task, *>) - & (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, Task, *>) - // & (>(cfn: [C, Fn], t1: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, Task, *>); - - declare type ForkFn = - & ContextForkFn - & (>(fn: Fn) => Generator<*, Task, *>) - & (>(fn: Fn, t1: T1) => Generator<*, Task, *>) - & (>(fn: Fn, t1: T1, t2: T2) => Generator<*, Task, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3) => Generator<*, Task, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4) => Generator<*, Task, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => Generator<*, Task, *>) - & (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => Generator<*, Task, *>) - // & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); +declare type ContextCallFn = (>( + cfn: [C, Fn], + ...rest: Array +) => Generator<*, R, *>) & + (>( + cfn: [C, Fn], + t1: T1, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + 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: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); + +declare type CallFn = ContextCallFn & + (>(fn: Fn, t1: T1) => Generator<*, R, *>) & + (>(fn: Fn) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, t2: T2) => Generator<*, R, *>) & + (>(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, ...args: Array) => Generator<*, R, *>); + +/* ------------------ CPS Stuff ------------------ */ + +declare type CallbackFn = (error: any, ret: R) => void; + +// declare type CpsFnSpread = (cb: CallbackFn, ...args: Array) => any; +declare type CpsFn0 = (cb: CallbackFn) => any; +declare type CpsFn1 = (t1: T1, CallbackFn) => any; +declare type CpsFn2 = (t1: T1, t2: T2, cb: CallbackFn) => any; +declare type CpsFn3 = (t1: T1, t2: T2, t3: T3, cb: CallbackFn) => any; +declare type CpsFn4 = (t1: T1, t2: T2, t3: T3, t4: T4, cb: CallbackFn) => any; +declare type CpsFn5 = ( + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + cb: CallbackFn, +) => any; +declare type CpsFn6 = ( + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + cb: CallbackFn, +) => any; + +declare type ContextCpsFn = (>( + cfn: [C, Fn], + ...rest: Array +) => Generator<*, R, *>) & + (>( + cfn: [C, Fn], + t1: T1, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + 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: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); + +declare type CpsFn = ContextCpsFn & + (>(fn: Fn, ...rest: Array) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, ...rest: Array) => Generator<*, R, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + ...rest: Array + ) => Generator<*, R, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + ...rest: Array + ) => Generator<*, R, *>); +// & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); + +/* ------------------ FORK Stuff ------------------ */ + +declare type ContextForkFn = (>( + cfn: [C, Fn], + ...rest: Array +) => Generator<*, Task, *>) & + (>( + cfn: [C, Fn], + t1: T1, + ...rest: Array + ) => Generator<*, Task, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + ...rest: Array + ) => Generator<*, Task, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + t3: T3, + ...rest: Array + ) => Generator<*, Task, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + t3: T3, + t4: T4, + ...rest: Array + ) => Generator<*, Task, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + ...rest: Array + ) => Generator<*, Task, *>) & + (>( + cfn: [C, Fn], + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + ...rest: Array + ) => Generator<*, Task, *>); +// & (>(cfn: [C, Fn], t1: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, Task, *>); + +declare type ForkFn = ContextForkFn & + (>(fn: Fn) => Generator<*, Task, *>) & + (>(fn: Fn, t1: T1) => Generator<*, Task, *>) & + (>(fn: Fn, t1: T1, t2: T2) => Generator<*, Task, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + ) => Generator<*, Task, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + ) => Generator<*, Task, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + ) => Generator<*, Task, *>) & + (>( + fn: Fn, + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + ) => Generator<*, Task, *>); +// & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); /* ------------------ Effects without return types ------------------ */ declare type PutFn = { - (action: T): Generator<*, void, *>; - (channel: Channel, action: T): Generator<*, void, *>; -} + (action: T): Generator<*, void, *>, + (channel: Channel, action: T): Generator<*, void, *>, +}; type ExtractReturnType = ((...args: any) => R) => R; -type ReturnType = $Call +type ReturnType = $Call; -declare type CancelledFn = () => Generator<*, bool, *>; +declare type CancelledFn = () => Generator<*, boolean, *>; declare type TakeFn = (pattern: P) => Generator<*, Action, *>; declare type RaceFnTest = (...args: any) => Generator<*, ReturnType, *>; -declare type RaceFn = (effects: R) => Generator<*, {+[name: $Keys]: ?Action}, *>; +declare type RaceFn = ( + effects: R, +) => Generator<*, { +[name: $Keys]: ?Action }, *>; +export interface IEffect { + +type: T; + +payload: P; + +combinator: C; +} +export type JoinEffect | Array>> = IEffect<"JOIN", T, false>; -declare export var select : SelectFn; -declare export var call : CallFn; -declare export var put : PutFn; -declare export var cps : CpsFn; -declare export var fork : ForkFn; -declare export var cancelled : CancelledFn; -declare export var take : TakeFn; -declare export var race : RaceFn; +declare export var select: SelectFn; +declare export var call: CallFn; +declare export var put: PutFn; +declare export var cps: CpsFn; +declare export var fork: ForkFn; +declare export var cancelled: CancelledFn; +declare export var take: TakeFn; +declare export var race: RaceFn; +declare export var join: { + // join(task) + // join([...tasks]) + >(task: T): JoinEffect, + >>(tasks: T): JoinEffect, + ..., +}; // Export the original saga function prefixed with a _ -declare export var _all : typeof typedAll; -declare export var _take : typeof typedTake; -declare export var _call : typeof typedCall; -declare export var _takeEvery : typeof typedTakeEvery; -declare export var _takeLeading : typeof typedTakeLeading; -declare export var _throttle : typeof typedThrottle; -declare export var _cancel : typeof typedCancel; -declare export var _delay : typeof typedDelay; +declare export var _all: typeof typedAll; +declare export var _take: typeof typedTake; +declare export var _call: typeof typedCall; +declare export var _takeEvery: typeof typedTakeEvery; +declare export var _takeLeading: typeof typedTakeLeading; +declare export var _throttle: typeof typedThrottle; +declare export var _cancel: typeof typedCancel; +declare export var _delay: typeof typedDelay; export type Saga = _Saga; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index b4cba95c3b..28a9ad304e 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -3,7 +3,7 @@ * @flow */ -import { type Saga } from "redux-saga"; +import { type Saga, type Task } from "redux-saga"; import Maybe from "data.maybe"; import { FlycamActions } from "oxalis/model/actions/flycam_actions"; @@ -16,7 +16,11 @@ import { } from "oxalis/model/sagas/save_saga_constants"; import type { Tracing, SkeletonTracing, Flycam, SaveQueueEntry } from "oxalis/store"; import { type UpdateAction } from "oxalis/model/sagas/update_actions"; -import { VolumeTracingSaveRelevantActions } from "oxalis/model/actions/volumetracing_actions"; +import { + VolumeTracingSaveRelevantActions, + type AddBucketToUndoAction, + type FinishAnnotationStrokeAction, +} from "oxalis/model/actions/volumetracing_actions"; import { _all, _delay, @@ -32,7 +36,7 @@ import { } from "oxalis/model/sagas/effect-generators"; import { SkeletonTracingSaveRelevantActions, - SkeletonTracingAction, + type SkeletonTracingAction, setTracingAction, centerActiveNodeAction, } from "oxalis/model/actions/skeletontracing_actions"; @@ -41,6 +45,8 @@ import { diffSkeletonTracing } from "oxalis/model/sagas/skeletontracing_saga"; import { diffVolumeTracing } from "oxalis/model/sagas/volumetracing_saga"; import { doWithToken } from "admin/admin_rest_api"; import { + type UndoAction, + type RedoAction, shiftSaveQueueAction, setSaveBusyAction, setLastSaveTimestampAction, @@ -73,12 +79,20 @@ type SkeletonUndoState = { type: "skeleton", data: SkeletonTracing }; type VolumeUndoState = { type: "volume", data: VolumeAnnotationBatch }; type UndoState = SkeletonUndoState | VolumeUndoState; +type racedActionsNeededForUndoRedo = { + skeletonUserAction: SkeletonTracingAction, + addBucketToUndoAction: ?AddBucketToUndoAction, + finishAnnotationStrokeAction: ?FinishAnnotationStrokeAction, + undo: UndoAction, + redo: RedoAction, +}; + export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; - let previousAction: ?Action = null; + let previousAction: ?any = null; let prevTracingMaybe: ?SkeletonTracing = null; - let pendingCompressions: Array> = []; + let pendingCompressions: Array> = []; let currentVolumeAnnotationBatch: VolumeAnnotationBatch = []; yield* take(["INITIALIZE_SKELETONTRACING", "INITIALIZE_VOLUMETRACING"]); @@ -90,25 +104,26 @@ export function* collectUndoStates(): Saga { finishAnnotationStrokeAction, undo, redo, - } = yield* race({ + } = ((yield* race({ skeletonUserAction: _take(SkeletonTracingSaveRelevantActions), addBucketToUndoAction: _take("ADD_BUCKET_TO_UNDO"), finishAnnotationStrokeAction: _take("FINISH_ANNOTATION_STROKE"), undo: _take("UNDO"), redo: _take("REDO"), - }); + }): any): racedActionsNeededForUndoRedo); if (skeletonUserAction || addBucketToUndoAction || finishAnnotationStrokeAction) { if (skeletonUserAction && prevTracingMaybe != null) { const skeletonUndoState = yield* call( getSkeletonTracingToUndoState, - skeletonUserAction, + ((skeletonUserAction: any): SkeletonTracingAction), prevTracingMaybe, previousAction, ); if (skeletonUndoState) { undoStack.push(skeletonUndoState); } - } else if (addBucketToUndoAction) { + previousAction = skeletonUserAction; + } else if (addBucketToUndoAction && addBucketToUndoAction.type === "ADD_BUCKET_TO_UNDO") { const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; pendingCompressions.push( yield* fork( @@ -130,9 +145,8 @@ export function* collectUndoStates(): Saga { if (undoStack.length > UNDO_HISTORY_SIZE) { undoStack.shift(); } - previousAction = skeletonUserAction; } else if (undo) { - if (undoStack.length && undoStack[undoStack.length - 1].type === "skeleton") { + if (undoStack.length > 0 && undoStack[undoStack.length - 1].type === "skeleton") { previousAction = null; } yield* call( @@ -143,7 +157,7 @@ export function* collectUndoStates(): Saga { messages["undo.no_undo"], ); } else if (redo) { - if (redoStack.length && redoStack[redoStack.length - 1].type === "skeleton") { + if (redoStack.length > 0 && redoStack[redoStack.length - 1].type === "skeleton") { previousAction = null; } yield* call( @@ -162,7 +176,7 @@ export function* collectUndoStates(): Saga { function* getSkeletonTracingToUndoState( skeletonUserAction: SkeletonTracingAction, prevTracing: SkeletonTracing, - previousAction: ?Action, + previousAction: ?SkeletonTracingAction, ): Saga { const curTracing = yield* select(state => enforceSkeletonTracing(state.tracing)); if (curTracing !== prevTracing) { @@ -177,7 +191,7 @@ function* compressBucketAndAddToUndoBatch( zoomedBucketAddress: Vector4, bucketData: BucketDataArray, undoBatch: VolumeAnnotationBatch, -) { +): Saga { const bucketDataAsByteArray = new Uint8Array( bucketData.buffer, bucketData.byteOffset, @@ -213,21 +227,23 @@ function* applyStateOfStack( stackToPushTo: Array, prevTracingMaybe: ?SkeletonTracing, warningMessage: string, -) { +): Saga { if (sourceStack.length <= 0) { Toast.info(warningMessage); return; } const stateToRestore = sourceStack.pop(); if (stateToRestore.type === "skeleton") { - stackToPushTo.push({ type: "skeleton", data: prevTracingMaybe }); + if (prevTracingMaybe != null) { + stackToPushTo.push({ type: "skeleton", data: prevTracingMaybe }); + } const newTracing = stateToRestore.data; yield* put(setTracingAction(newTracing)); yield* put(centerActiveNodeAction()); } else if (stateToRestore.type === "volume") { const volumeBatchToApply = stateToRestore.data; const currentVolumeState = yield* call( - applyVolumeAnnotationBAtchAndGetMatchingUndo, + applyVolumeAnnotationBatchAndGetMatchingUndo, volumeBatchToApply, stackToPushTo, ); @@ -235,17 +251,20 @@ function* applyStateOfStack( } } -function* applyVolumeAnnotationBAtchAndGetMatchingUndo( +function* applyVolumeAnnotationBatchAndGetMatchingUndo( volumeAnnotationBatch: VolumeAnnotationBatch, ): Saga { - const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]); + const segmentationLayer = Model.getSegmentationLayer(); if (!segmentationLayer) { throw new Error("Undoing a volume annotation but no volume layer exists."); } const { cube } = segmentationLayer; const allCompressedBucketsOfCurrentState: VolumeAnnotationBatch = []; for (const { zoomedBucketAddress, data: compressedBucketData } of volumeAnnotationBatch) { - const bucket = yield* call([cube, cube.getOrCreateBucket], zoomedBucketAddress); + const bucket = cube.getOrCreateBucket(zoomedBucketAddress); + if (bucket.type === "null") { + continue; + } const bucketData = bucket.getData(); yield* call( compressBucketAndAddToUndoBatch, @@ -254,7 +273,9 @@ function* applyVolumeAnnotationBAtchAndGetMatchingUndo( allCompressedBucketsOfCurrentState, ); const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); - yield* call([bucket, bucket.setData], decompressedBucketData); + if (decompressedBucketData) { + bucket.setData(decompressedBucketData); + } } return { type: "volume", From 9bca52feb07182f82e366b6ba9c6994e87489d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Wed, 19 Aug 2020 11:31:34 +0200 Subject: [PATCH 12/23] add better flow defintion for saga effect join --- .../oxalis/model/sagas/effect-generators.js.flow | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow index 94a03e0ee3..1df338841c 100644 --- a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow +++ b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow @@ -461,6 +461,8 @@ export interface IEffect { } export type JoinEffect | Array>> = IEffect<"JOIN", T, false>; +declare type JoinFn = | Array>>(tasks: T) => Generator<*, T, *>; + declare export var select: SelectFn; declare export var call: CallFn; declare export var put: PutFn; @@ -469,13 +471,7 @@ declare export var fork: ForkFn; declare export var cancelled: CancelledFn; declare export var take: TakeFn; declare export var race: RaceFn; -declare export var join: { - // join(task) - // join([...tasks]) - >(task: T): JoinEffect, - >>(tasks: T): JoinEffect, - ..., -}; +declare export var join: JoinFn; // Export the original saga function prefixed with a _ declare export var _all: typeof typedAll; From d58e285da42b8260ea338525ec2d2958fc44af34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 20 Aug 2020 11:58:48 +0200 Subject: [PATCH 13/23] apply feedback --- .../model/bucket_data_handling/bucket.js | 11 +++--- .../oxalis/model/sagas/save_saga.js | 36 +++++++++---------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index d93bb97e35..256df33a2a 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -30,8 +30,8 @@ export const BucketStateEnum = { export type BucketStateEnumType = $Keys; export type BucketDataArray = Uint8Array | Uint16Array | Uint32Array | Float32Array; // This set saves whether a bucket is already added to the current undo volume batch -// and gets cleared by the save saga after a annotation step has finished. -export const bucketAlreadyInUndoState: Set = new Set(); +// and gets cleared by the save saga after an annotation step has finished. +export const bucketsAlreadyInUndoState: Set = new Set(); export const bucketDebuggingFlags = { // For visualizing buckets which are passed to the GPU @@ -166,12 +166,15 @@ export class DataBucket { label(labelFunc: BucketDataArray => void) { const bucketData = this.getOrCreateData(); - if (!bucketAlreadyInUndoState.has(this.zoomedAddress)) { + const zoomedAddressAsString = `${this.zoomedAddress[0]},${this.zoomedAddress[1]},${ + this.zoomedAddress[2] + },${this.zoomedAddress[3]}`; + if (!bucketsAlreadyInUndoState.has(zoomedAddressAsString)) { const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; const dataClone = new TypedArrayClass(bucketData); Store.dispatch(addBucketToUndoAction(this.zoomedAddress, dataClone)); + bucketsAlreadyInUndoState.add(zoomedAddressAsString); } - bucketAlreadyInUndoState.add(this.zoomedAddress); labelFunc(bucketData); this.dirty = true; this.throttledTriggerLabeled(); diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 28a9ad304e..5b3cc98e67 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -66,7 +66,7 @@ import type { Vector4 } from "oxalis/constants"; import compressLz4Block from "oxalis/workers/byte_array_lz4_compression.worker"; import { createWorker } from "oxalis/workers/comlink_wrapper"; import { - bucketAlreadyInUndoState, + bucketsAlreadyInUndoState, type BucketDataArray, } from "oxalis/model/bucket_data_handling/bucket"; import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; @@ -80,23 +80,23 @@ type VolumeUndoState = { type: "volume", data: VolumeAnnotationBatch }; type UndoState = SkeletonUndoState | VolumeUndoState; type racedActionsNeededForUndoRedo = { - skeletonUserAction: SkeletonTracingAction, + skeletonUserAction: ?SkeletonTracingAction, addBucketToUndoAction: ?AddBucketToUndoAction, finishAnnotationStrokeAction: ?FinishAnnotationStrokeAction, - undo: UndoAction, - redo: RedoAction, + undo: ?UndoAction, + redo: ?RedoAction, }; export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; let previousAction: ?any = null; - let prevTracingMaybe: ?SkeletonTracing = null; + let prevSkeletonTracingOrNull: ?SkeletonTracing = null; let pendingCompressions: Array> = []; let currentVolumeAnnotationBatch: VolumeAnnotationBatch = []; yield* take(["INITIALIZE_SKELETONTRACING", "INITIALIZE_VOLUMETRACING"]); - prevTracingMaybe = yield* select(state => state.tracing.skeleton); + prevSkeletonTracingOrNull = yield* select(state => state.tracing.skeleton); while (true) { const { skeletonUserAction, @@ -112,18 +112,18 @@ export function* collectUndoStates(): Saga { redo: _take("REDO"), }): any): racedActionsNeededForUndoRedo); if (skeletonUserAction || addBucketToUndoAction || finishAnnotationStrokeAction) { - if (skeletonUserAction && prevTracingMaybe != null) { + if (skeletonUserAction && prevSkeletonTracingOrNull != null) { const skeletonUndoState = yield* call( getSkeletonTracingToUndoState, ((skeletonUserAction: any): SkeletonTracingAction), - prevTracingMaybe, + prevSkeletonTracingOrNull, previousAction, ); if (skeletonUndoState) { undoStack.push(skeletonUndoState); } previousAction = skeletonUserAction; - } else if (addBucketToUndoAction && addBucketToUndoAction.type === "ADD_BUCKET_TO_UNDO") { + } else if (addBucketToUndoAction) { const { zoomedBucketAddress, bucketData } = addBucketToUndoAction; pendingCompressions.push( yield* fork( @@ -135,7 +135,7 @@ export function* collectUndoStates(): Saga { ); } else if (finishAnnotationStrokeAction) { yield* join([...pendingCompressions]); - bucketAlreadyInUndoState.clear(); + bucketsAlreadyInUndoState.clear(); undoStack.push({ type: "volume", data: currentVolumeAnnotationBatch }); currentVolumeAnnotationBatch = []; pendingCompressions = []; @@ -153,7 +153,7 @@ export function* collectUndoStates(): Saga { applyStateOfStack, undoStack, redoStack, - prevTracingMaybe, + prevSkeletonTracingOrNull, messages["undo.no_undo"], ); } else if (redo) { @@ -164,12 +164,12 @@ export function* collectUndoStates(): Saga { applyStateOfStack, redoStack, undoStack, - prevTracingMaybe, + prevSkeletonTracingOrNull, messages["undo.no_redo"], ); } // We need the updated tracing here - prevTracingMaybe = yield* select(state => state.tracing.skeleton); + prevSkeletonTracingOrNull = yield* select(state => state.tracing.skeleton); } } @@ -225,7 +225,7 @@ function shouldAddToUndoStack(currentUserAction: Action, previousAction: ?Action function* applyStateOfStack( sourceStack: Array, stackToPushTo: Array, - prevTracingMaybe: ?SkeletonTracing, + prevSkeletonTracingOrNull: ?SkeletonTracing, warningMessage: string, ): Saga { if (sourceStack.length <= 0) { @@ -234,8 +234,8 @@ function* applyStateOfStack( } const stateToRestore = sourceStack.pop(); if (stateToRestore.type === "skeleton") { - if (prevTracingMaybe != null) { - stackToPushTo.push({ type: "skeleton", data: prevTracingMaybe }); + if (prevSkeletonTracingOrNull != null) { + stackToPushTo.push({ type: "skeleton", data: prevSkeletonTracingOrNull }); } const newTracing = stateToRestore.data; yield* put(setTracingAction(newTracing)); @@ -243,7 +243,7 @@ function* applyStateOfStack( } else if (stateToRestore.type === "volume") { const volumeBatchToApply = stateToRestore.data; const currentVolumeState = yield* call( - applyVolumeAnnotationBatchAndGetMatchingUndo, + applyAndGetRevertingVolumeBatch, volumeBatchToApply, stackToPushTo, ); @@ -251,7 +251,7 @@ function* applyStateOfStack( } } -function* applyVolumeAnnotationBatchAndGetMatchingUndo( +function* applyAndGetRevertingVolumeBatch( volumeAnnotationBatch: VolumeAnnotationBatch, ): Saga { const segmentationLayer = Model.getSegmentationLayer(); From 9a0023cbac66805beb0ee0dfd5d12b510303118a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 20 Aug 2020 12:32:18 +0200 Subject: [PATCH 14/23] remove useless type cast & fix typo --- .../javascripts/oxalis/model/sagas/effect-generators.js.flow | 2 +- frontend/javascripts/oxalis/model/sagas/save_saga.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow index 4336b23ecc..e7f36791f5 100644 --- a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow +++ b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow @@ -26,7 +26,7 @@ import type { OxalisState } from "oxalis/store"; declare type Context = Object; -// We disable fucntion spreads, as it weakens the typechecks (wrong parameter +// We disable function spreads, as it weakens the typechecks (wrong parameter // won't be noticed count) // declare type FnSpread = (...args: Array) => Promise | R; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 5b3cc98e67..a6e89d79c7 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -115,7 +115,7 @@ export function* collectUndoStates(): Saga { if (skeletonUserAction && prevSkeletonTracingOrNull != null) { const skeletonUndoState = yield* call( getSkeletonTracingToUndoState, - ((skeletonUserAction: any): SkeletonTracingAction), + skeletonUserAction, prevSkeletonTracingOrNull, previousAction, ); From 83a14674b115344ba885e17ec18683055f2f0ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 20 Aug 2020 15:00:13 +0200 Subject: [PATCH 15/23] various fixes - notice every brush step as an undo step - undo triggers pushqueue - prevent undoing volume in merger mode --- frontend/javascripts/messages.js | 2 ++ .../oxalis/model/bucket_data_handling/data_cube.js | 13 +++++++++++++ .../oxalis/model/reducers/annotation_reducer.js | 1 + .../javascripts/oxalis/model/sagas/save_saga.js | 11 ++++++++++- .../oxalis/model/sagas/volumetracing_saga.js | 2 +- 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index fe27d598bd..fd214dde7a 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -97,6 +97,8 @@ instead. Only enable this option if you understand its effect. All layers will n "tracing.branchpoint_set": "Branchpoint set", "tracing.branchpoint_jump_twice": "You didn't add a node after jumping to this branchpoint, do you really want to jump again?", + "tracing.edit_volume_in_merger_mode": + "You are about to edit the volume data while the merger mode is activated. This is not allowed.", "tracing.segmentation_zoom_warning": "Segmentation data and volume annotation is only fully supported at a smaller zoom level.", "tracing.uint64_segmentation_warning": diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js index d6033439a3..145176027d 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js @@ -368,6 +368,19 @@ class DataCube { } } + setBucketData(zoomedAddress: Vector4, data: Uint8Array) { + const bucket = this.getOrCreateBucket(zoomedAddress); + if (bucket.type === "null") { + return; + } + bucket.setData(data); + this.pushQueue.insert(bucket); + } + + triggerPushQueue() { + this.pushQueue.push(); + } + hasDataAtPositionAndZoomStep(voxel: Vector3, zoomStep: number = 0) { return this.getBucket(this.positionToZoomedAddress(voxel, zoomStep)).hasData(); } diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index aca1739359..6183346668 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -30,6 +30,7 @@ const updateUserBoundingBoxes = (state: OxalisState, userBoundingBoxes: Array state.temporaryConfiguration.isMergerModeEnabled, + ); + if (isMergerModeEnabled) { + Toast.info(messages["tracing.edit_volume_in_merger_mode"]); + return; + } const volumeBatchToApply = stateToRestore.data; const currentVolumeState = yield* call( applyAndGetRevertingVolumeBatch, @@ -274,9 +281,11 @@ function* applyAndGetRevertingVolumeBatch( ); const decompressedBucketData = yield* call(byteArrayToLz4Array, compressedBucketData, false); if (decompressedBucketData) { - bucket.setData(decompressedBucketData); + // Set the new bucket data to add the bucket directly to the pushqueue. + cube.setBucketData(zoomedBucketAddress, decompressedBucketData); } } + cube.triggerPushQueue(); return { type: "volume", data: allCompressedBucketsOfCurrentState, diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index 812d634bcb..e9bddff9c0 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -139,6 +139,7 @@ export function* editVolumeLayerAsync(): Generator { } yield* call(finishLayer, currentLayer, activeTool, contourTracingMode); + yield* put(finishAnnotationStrokeAction()); } } @@ -255,7 +256,6 @@ export function* finishLayer( yield* put(updateDirectionAction(layer.getCentroid())); yield* put(resetContourAction()); - yield* put(finishAnnotationStrokeAction()); } export function* disallowVolumeTracingWarning(): Saga<*> { From 5c1a7e2bf7fb7e773622ad191cdde8733adcb520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 20 Aug 2020 15:05:06 +0200 Subject: [PATCH 16/23] pretty flow file --- .prettierrc | 9 +- .../model/sagas/effect-generators.js.flow | 372 +++--------------- 2 files changed, 53 insertions(+), 328 deletions(-) diff --git a/.prettierrc b/.prettierrc index 288a5ab428..978a71006f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,12 @@ { "trailingComma": "all", "printWidth": 100, + "overrides": [ + { + "files": "effect-generators.js.flow", + "options": { + "printWidth": 200 + } + } + ] } - diff --git a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow index e7f36791f5..30109de161 100644 --- a/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow +++ b/frontend/javascripts/oxalis/model/sagas/effect-generators.js.flow @@ -1,14 +1,6 @@ // @flow -import { - type Channel, - type Effect, - type Pattern, - type Task, - type Saga as _Saga, - channel, - effects, -} from "redux-saga"; +import { type Channel, type Effect, type Pattern, type Task, type Saga as _Saga, channel, effects } from "redux-saga"; import { all as typedAll, race as typedRace, @@ -34,27 +26,9 @@ declare type Fn0 = () => Promise | Generator<*, R, *> | R; declare type Fn1 = (t1: T1) => Promise | Generator<*, R, *> | R; declare type Fn2 = (t1: T1, t2: T2) => Promise | Generator<*, R, *> | R; declare type Fn3 = (t1: T1, t2: T2, t3: T3) => Promise | Generator<*, R, *> | R; -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 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; /* ------------------ SELECT Stuff ------------------ */ @@ -63,35 +37,11 @@ declare type SelectFn0 = (state: OxalisState) => R; declare type SelectFn1 = (state: OxalisState, t1: T1) => R; declare type SelectFn2 = (state: OxalisState, t1: T1, t2: T2) => R; declare type SelectFn3 = (state: OxalisState, t1: T1, t2: T2, t3: T3) => R; -declare type SelectFn4 = ( - state: OxalisState, - t1: T1, - t2: T2, - t3: T3, - t4: T4, -) => R; -declare type SelectFn5 = ( - state: OxalisState, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, -) => R; -declare type SelectFn6 = ( - state: OxalisState, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, -) => R; +declare type SelectFn4 = (state: OxalisState, t1: T1, t2: T2, t3: T3, t4: T4) => R; +declare type SelectFn5 = (state: OxalisState, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R; +declare type SelectFn6 = (state: OxalisState, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => R; -declare type SelectFn = >( - selector: Fn, - ...rest: Array -) => Generator<*, R, *>; +declare type SelectFn = >(selector: Fn, ...rest: Array) => Generator<*, R, *>; // We are only using SelectFn0 and the others lead to flow errors // & (>(selector: Fn, t1: T1, ...rest: Array) => Generator<*, R, *>) // & (>(selector: Fn, t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) @@ -103,55 +53,13 @@ declare type SelectFn = >( /* ------------------ CALL Stuff ------------------ */ -declare type ContextCallFn = (>( - cfn: [C, Fn], - ...rest: Array -) => Generator<*, R, *>) & - (>( - cfn: [C, Fn], - t1: T1, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - 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, *>); +declare type ContextCallFn = (>(cfn: [C, Fn], ...rest: Array) => Generator<*, R, *>) & + (>(cfn: [C, Fn], t1: T1, ...rest: Array) => Generator<*, R, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) & + (>(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: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); declare type CallFn = ContextCallFn & @@ -159,30 +67,9 @@ declare type CallFn = ContextCallFn & (>(fn: Fn) => Generator<*, R, *>) & (>(fn: Fn, t1: T1, t2: T2) => Generator<*, R, *>) & (>(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) => 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, ...args: Array) => Generator<*, R, *>); /* ------------------ CPS Stuff ------------------ */ @@ -195,207 +82,47 @@ declare type CpsFn1 = (t1: T1, CallbackFn) => any; declare type CpsFn2 = (t1: T1, t2: T2, cb: CallbackFn) => any; declare type CpsFn3 = (t1: T1, t2: T2, t3: T3, cb: CallbackFn) => any; declare type CpsFn4 = (t1: T1, t2: T2, t3: T3, t4: T4, cb: CallbackFn) => any; -declare type CpsFn5 = ( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - cb: CallbackFn, -) => any; -declare type CpsFn6 = ( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - cb: CallbackFn, -) => any; - -declare type ContextCpsFn = (>( - cfn: [C, Fn], - ...rest: Array -) => Generator<*, R, *>) & - (>( - cfn: [C, Fn], - t1: T1, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - 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, *>); +declare type CpsFn5 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, cb: CallbackFn) => any; +declare type CpsFn6 = (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, cb: CallbackFn) => any; + +declare type ContextCpsFn = (>(cfn: [C, Fn], ...rest: Array) => Generator<*, R, *>) & + (>(cfn: [C, Fn], t1: T1, ...rest: Array) => Generator<*, R, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) & + (>(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: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, R, *>); declare type CpsFn = ContextCpsFn & (>(fn: Fn, ...rest: Array) => Generator<*, R, *>) & (>(fn: Fn, t1: T1, ...rest: Array) => Generator<*, R, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - ...rest: Array - ) => Generator<*, R, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - ...rest: Array - ) => Generator<*, R, *>); + (>(fn: Fn, t1: T1, t2: T2, ...rest: Array) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, ...rest: Array) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, ...rest: Array) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, ...rest: Array) => Generator<*, R, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, R, *>); // & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); /* ------------------ FORK Stuff ------------------ */ -declare type ContextForkFn = (>( - cfn: [C, Fn], - ...rest: Array -) => Generator<*, Task, *>) & - (>( - cfn: [C, Fn], - t1: T1, - ...rest: Array - ) => Generator<*, Task, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - ...rest: Array - ) => Generator<*, Task, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - t3: T3, - ...rest: Array - ) => Generator<*, Task, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - t3: T3, - t4: T4, - ...rest: Array - ) => Generator<*, Task, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - ...rest: Array - ) => Generator<*, Task, *>) & - (>( - cfn: [C, Fn], - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - ...rest: Array - ) => Generator<*, Task, *>); +declare type ContextForkFn = (>(cfn: [C, Fn], ...rest: Array) => Generator<*, Task, *>) & + (>(cfn: [C, Fn], t1: T1, ...rest: Array) => Generator<*, Task, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, ...rest: Array) => Generator<*, Task, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, ...rest: Array) => Generator<*, Task, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, ...rest: Array) => Generator<*, Task, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, ...rest: Array) => Generator<*, Task, *>) & + (>(cfn: [C, Fn], t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, ...rest: Array) => Generator<*, Task, *>); // & (>(cfn: [C, Fn], t1: T, t2: T, t3: T, t4: T, t5: T, t6: T, ...args: Array) => Generator<*, Task, *>); declare type ForkFn = ContextForkFn & (>(fn: Fn) => Generator<*, Task, *>) & (>(fn: Fn, t1: T1) => Generator<*, Task, *>) & (>(fn: Fn, t1: T1, t2: T2) => Generator<*, Task, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - ) => Generator<*, Task, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - ) => Generator<*, Task, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - ) => Generator<*, Task, *>) & - (>( - fn: Fn, - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - ) => Generator<*, Task, *>); + (>(fn: Fn, t1: T1, t2: T2, t3: T3) => Generator<*, Task, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4) => Generator<*, Task, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => Generator<*, Task, *>) & + (>(fn: Fn, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => Generator<*, Task, *>); // & (>(fn: Fn, ...args: Array) => Generator<*, R, *>); /* ------------------ Effects without return types ------------------ */ @@ -412,16 +139,7 @@ declare type CancelledFn = () => Generator<*, boolean, *>; declare type TakeFn = (pattern: P) => Generator<*, Action, *>; declare type RaceFnTest = (...args: any) => Generator<*, ReturnType, *>; -declare type RaceFn = ( - effects: R, -) => Generator<*, { +[name: $Keys]: ?Action }, *>; - -export interface IEffect { - +type: T; - +payload: P; - +combinator: C; -} -export type JoinEffect | Array>> = IEffect<"JOIN", T, false>; +declare type RaceFn = (effects: R) => Generator<*, { +[name: $Keys]: ?Action }, *>; declare type JoinFn = | Array>>(tasks: T) => Generator<*, T, *>; From eb4e09928fdae2e16d2057d62c229189b0ef26d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 20 Aug 2020 15:08:25 +0200 Subject: [PATCH 17/23] fix tests --- frontend/javascripts/test/sagas/volumetracing_saga.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/javascripts/test/sagas/volumetracing_saga.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga.spec.js index 8dab780e68..9e6ee951da 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga.spec.js @@ -192,6 +192,5 @@ test("finishLayer saga should emit resetContourAction and then be done (saga tes saga.next(); saga.next(); expectValueDeepEqual(t, saga.next(), put(resetContourAction)); - expectValueDeepEqual(t, saga.next(), put(finishAnnotationStrokeAction)); t.true(saga.next().done); }); From e64a7af53261497dd412ad107943d4fc72dd0df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 20 Aug 2020 15:37:13 +0200 Subject: [PATCH 18/23] fix linting --- frontend/javascripts/test/sagas/volumetracing_saga.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/javascripts/test/sagas/volumetracing_saga.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga.spec.js index 9e6ee951da..7f2eb87256 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga.spec.js @@ -67,7 +67,6 @@ const startEditingAction = VolumeTracingActions.startEditingAction([0, 0, 0], Or const addToLayerActionFn = VolumeTracingActions.addToLayerAction; const finishEditingAction = VolumeTracingActions.finishEditingAction(); const resetContourAction = VolumeTracingActions.resetContourAction(); -const finishAnnotationStrokeAction = VolumeTracingActions.finishAnnotationStrokeAction(); test("VolumeTracingSaga shouldn't do anything if unchanged (saga test)", t => { const saga = saveTracingTypeAsync("volume"); From c8813f3556ef262759ace28b04c02e1f089b7607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 21 Aug 2020 11:08:57 +0200 Subject: [PATCH 19/23] apply feedback --- frontend/javascripts/messages.js | 2 +- .../oxalis/model/bucket_data_handling/bucket.js | 4 +--- .../oxalis/model/bucket_data_handling/data_cube.js | 4 ++-- .../oxalis/model/reducers/annotation_reducer.js | 1 - frontend/javascripts/oxalis/model/sagas/save_saga.js | 7 ++----- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index fd214dde7a..8baee2d433 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -98,7 +98,7 @@ instead. Only enable this option if you understand its effect. All layers will n "tracing.branchpoint_jump_twice": "You didn't add a node after jumping to this branchpoint, do you really want to jump again?", "tracing.edit_volume_in_merger_mode": - "You are about to edit the volume data while the merger mode is activated. This is not allowed.", + "The volume annotation would be changed by this action. This is not allowed while merger mode is active.", "tracing.segmentation_zoom_warning": "Segmentation data and volume annotation is only fully supported at a smaller zoom level.", "tracing.uint64_segmentation_warning": diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index 256df33a2a..55659c4de9 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -166,9 +166,7 @@ export class DataBucket { label(labelFunc: BucketDataArray => void) { const bucketData = this.getOrCreateData(); - const zoomedAddressAsString = `${this.zoomedAddress[0]},${this.zoomedAddress[1]},${ - this.zoomedAddress[2] - },${this.zoomedAddress[3]}`; + const zoomedAddressAsString = this.zoomedAddress.toString(); if (!bucketsAlreadyInUndoState.has(zoomedAddressAsString)) { const TypedArrayClass = getConstructorForElementClass(this.elementClass)[0]; const dataClone = new TypedArrayClass(bucketData); diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js index 145176027d..b5cfc2db6e 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js @@ -331,8 +331,7 @@ class DataCube { this.labelVoxel(voxel, label, activeCellId); } - this.pushQueue.push(); - this.trigger("volumeLabeled"); + this.triggerPushQueue(); } labelVoxel(voxel: Vector3, label: number, activeCellId: ?number): void { @@ -379,6 +378,7 @@ class DataCube { triggerPushQueue() { this.pushQueue.push(); + this.trigger("volumeLabeled"); } hasDataAtPositionAndZoomStep(voxel: Vector3, zoomStep: number = 0) { diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 6183346668..aca1739359 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -30,7 +30,6 @@ const updateUserBoundingBoxes = (state: OxalisState, userBoundingBoxes: Array Date: Fri, 21 Aug 2020 11:32:48 +0200 Subject: [PATCH 20/23] cache node radius to avoid clearing the redo stack --- .../oxalis/view/settings/user_settings_view.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/oxalis/view/settings/user_settings_view.js b/frontend/javascripts/oxalis/view/settings/user_settings_view.js index 91b1c7b967..f09287220b 100644 --- a/frontend/javascripts/oxalis/view/settings/user_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/user_settings_view.js @@ -49,6 +49,8 @@ import * as Utils from "libs/utils"; const { Panel } = Collapse; +const DEFAULT_NODE_RADIUS = 0; + type UserSettingsViewProps = { userConfiguration: UserConfiguration, tracing: Tracing, @@ -73,6 +75,7 @@ type UserSettingsViewProps = { class UserSettingsView extends PureComponent { onChangeUser: { [$Keys]: Function }; + cachedNodeRadius: number = DEFAULT_NODE_RADIUS; componentWillMount() { // cache onChange handler @@ -265,9 +268,11 @@ class UserSettingsView extends PureComponent { const skeletonTracing = enforceSkeletonTracing(this.props.tracing); const activeNodeId = skeletonTracing.activeNodeId != null ? skeletonTracing.activeNodeId : ""; const activeTreeId = skeletonTracing.activeTreeId != null ? skeletonTracing.activeTreeId : ""; - const activeNodeRadius = getActiveNode(skeletonTracing) + // We cache the node radius to avoid resetting it to 0 when all nodes got removed. + // This avoids triggering a new skeleton action, that would clear the redo stack (unwanted). + this.cachedNodeRadius = getActiveNode(skeletonTracing) .map(activeNode => activeNode.radius) - .getOrElse(0); + .getOrElse(this.cachedNodeRadius); const isMergerModeSupported = hasSegmentation(this.props.dataset); panels.push( @@ -286,9 +291,11 @@ class UserSettingsView extends PureComponent { min={userSettings.nodeRadius.minimum} max={userSettings.nodeRadius.maximum} roundTo={0} - value={activeNodeRadius} + value={this.cachedNodeRadius} onChange={this.props.onChangeRadius} - disabled={this.props.userConfiguration.overrideNodeRadius || activeNodeRadius === 0} + disabled={ + this.props.userConfiguration.overrideNodeRadius || this.cachedNodeRadius === 0 + } /> Date: Fri, 21 Aug 2020 13:20:59 +0200 Subject: [PATCH 21/23] fix merge --- .../javascripts/oxalis/model/actions/volumetracing_actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index cf735641e9..cc7f373cc1 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -62,7 +62,7 @@ export type VolumeTracingAction = | CopySegmentationLayerAction | InferSegmentationInViewportAction | SetContourTracingModeAction - | AddBucketToUndoAction; + | AddBucketToUndoAction | RemoveFallbackLayerAction; export const VolumeTracingSaveRelevantActions = [ From 8a27a7f97ce633f7a5b35fb2902fb65786f3b90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 24 Aug 2020 11:31:41 +0200 Subject: [PATCH 22/23] undo node radius fix && only clear redo stack if a relevant action is done --- .../javascripts/oxalis/model/sagas/save_saga.js | 9 +++++++-- .../oxalis/view/settings/user_settings_view.js | 15 ++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index e79b8448f7..e75eef26a6 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -112,6 +112,8 @@ export function* collectUndoStates(): Saga { redo: _take("REDO"), }): any): racedActionsNeededForUndoRedo); if (skeletonUserAction || addBucketToUndoAction || finishAnnotationStrokeAction) { + let shouldClearRedoState = + addBucketToUndoAction != null || finishAnnotationStrokeAction != null; if (skeletonUserAction && prevSkeletonTracingOrNull != null) { const skeletonUndoState = yield* call( getSkeletonTracingToUndoState, @@ -120,6 +122,7 @@ export function* collectUndoStates(): Saga { previousAction, ); if (skeletonUndoState) { + shouldClearRedoState = true; undoStack.push(skeletonUndoState); } previousAction = skeletonUserAction; @@ -140,8 +143,10 @@ export function* collectUndoStates(): Saga { currentVolumeAnnotationBatch = []; pendingCompressions = []; } - // Clear the redo stack when a new action is executed. - redoStack.splice(0); + if (shouldClearRedoState) { + // Clear the redo stack when a new action is executed. + redoStack.splice(0); + } if (undoStack.length > UNDO_HISTORY_SIZE) { undoStack.shift(); } diff --git a/frontend/javascripts/oxalis/view/settings/user_settings_view.js b/frontend/javascripts/oxalis/view/settings/user_settings_view.js index f09287220b..91b1c7b967 100644 --- a/frontend/javascripts/oxalis/view/settings/user_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/user_settings_view.js @@ -49,8 +49,6 @@ import * as Utils from "libs/utils"; const { Panel } = Collapse; -const DEFAULT_NODE_RADIUS = 0; - type UserSettingsViewProps = { userConfiguration: UserConfiguration, tracing: Tracing, @@ -75,7 +73,6 @@ type UserSettingsViewProps = { class UserSettingsView extends PureComponent { onChangeUser: { [$Keys]: Function }; - cachedNodeRadius: number = DEFAULT_NODE_RADIUS; componentWillMount() { // cache onChange handler @@ -268,11 +265,9 @@ class UserSettingsView extends PureComponent { const skeletonTracing = enforceSkeletonTracing(this.props.tracing); const activeNodeId = skeletonTracing.activeNodeId != null ? skeletonTracing.activeNodeId : ""; const activeTreeId = skeletonTracing.activeTreeId != null ? skeletonTracing.activeTreeId : ""; - // We cache the node radius to avoid resetting it to 0 when all nodes got removed. - // This avoids triggering a new skeleton action, that would clear the redo stack (unwanted). - this.cachedNodeRadius = getActiveNode(skeletonTracing) + const activeNodeRadius = getActiveNode(skeletonTracing) .map(activeNode => activeNode.radius) - .getOrElse(this.cachedNodeRadius); + .getOrElse(0); const isMergerModeSupported = hasSegmentation(this.props.dataset); panels.push( @@ -291,11 +286,9 @@ class UserSettingsView extends PureComponent { min={userSettings.nodeRadius.minimum} max={userSettings.nodeRadius.maximum} roundTo={0} - value={this.cachedNodeRadius} + value={activeNodeRadius} onChange={this.props.onChangeRadius} - disabled={ - this.props.userConfiguration.overrideNodeRadius || this.cachedNodeRadius === 0 - } + disabled={this.props.userConfiguration.overrideNodeRadius || activeNodeRadius === 0} /> Date: Mon, 24 Aug 2020 17:21:38 +0200 Subject: [PATCH 23/23] add changelog entry --- CHANGELOG.unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 5abac25e6b..af89cd4b21 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Added - 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) +- Added the possibility to undo and redo volume annotation strokes. [#4771](https://github.com/scalableminds/webknossos/pull/4771) ### Changed -