From c55cbdefe59060befc375ff4248a95c996344e4b Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 25 Mar 2020 20:16:38 +0300 Subject: [PATCH] Undo/redo returns frame where was a change (as it was done in previous version) --- cvat-core/src/annotations-collection.js | 11 +- cvat-core/src/annotations-history.js | 7 +- cvat-core/src/annotations-objects.js | 100 +++++------ cvat-core/src/object-state.js | 14 +- cvat-core/src/session.js | 1 + cvat-ui/src/actions/annotation-actions.ts | 168 +++++++++--------- .../annotation-page/top-bar/top-bar.tsx | 4 +- cvat-ui/src/reducers/interfaces.ts | 4 +- 8 files changed, 163 insertions(+), 146 deletions(-) diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 2135780c..e3ba2735 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -427,7 +427,10 @@ for (const object of objectsForMerge) { object.removed = true; } - }, [...objectsForMerge.map((object) => object.clientID), trackModel.clientID]); + }, [ + ...objectsForMerge + .map((object) => object.clientID), trackModel.clientID, + ], objectStates[0].frame); } split(objectState, frame) { @@ -522,7 +525,7 @@ object.removed = true; prevTrack.removed = false; nextTrack.removed = false; - }, [object.clientID, prevTrack.clientID, nextTrack.clientID]); + }, [object.clientID, prevTrack.clientID, nextTrack.clientID], frame); } group(objectStates, reset) { @@ -554,7 +557,7 @@ objectsForGroup.forEach((object, idx) => { object.group = redoGroups[idx]; }); - }, objectsForGroup.map((object) => object.clientID)); + }, objectsForGroup.map((object) => object.clientID), objectStates[0].frame); return groupIdx; } @@ -790,7 +793,7 @@ importedArray.forEach((object) => { object.removed = false; }); - }, importedArray.map((object) => object.clientID)); + }, importedArray.map((object) => object.clientID), objectStates[0].frame); } select(objectStates, x, y) { diff --git a/cvat-core/src/annotations-history.js b/cvat-core/src/annotations-history.js index d98973e5..4fdbf34c 100644 --- a/cvat-core/src/annotations-history.js +++ b/cvat-core/src/annotations-history.js @@ -12,17 +12,18 @@ class AnnotationHistory { get() { return { - undo: this._undo.map((undo) => undo.action), - redo: this._redo.map((redo) => redo.action), + undo: this._undo.map((undo) => [undo.action, undo.frame]), + redo: this._redo.map((redo) => [redo.action, redo.frame]), }; } - do(action, undo, redo, clientIDs) { + do(action, undo, redo, clientIDs, frame) { const actionItem = { clientIDs, action, undo, redo, + frame, }; this._undo = this._undo.slice(-MAX_HISTORY_LENGTH + 1); diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 245d8e37..47c65fb9 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -178,7 +178,7 @@ injection.groups.max = Math.max(injection.groups.max, this.group); } - _saveLock(lock) { + _saveLock(lock, frame) { const undoLock = this.lock; const redoLock = lock; @@ -186,12 +186,12 @@ this.lock = undoLock; }, () => { this.lock = redoLock; - }, [this.clientID]); + }, [this.clientID], frame); this.lock = lock; } - _saveColor(color) { + _saveColor(color, frame) { const undoColor = this.color; const redoColor = color; @@ -199,12 +199,12 @@ this.color = undoColor; }, () => { this.color = redoColor; - }, [this.clientID]); + }, [this.clientID], frame); this.color = color; } - _saveHidden(hidden) { + _saveHidden(hidden, frame) { const undoHidden = this.hidden; const redoHidden = hidden; @@ -212,12 +212,12 @@ this.hidden = undoHidden; }, () => { this.hidden = redoHidden; - }, [this.clientID]); + }, [this.clientID], frame); this.hidden = hidden; } - _saveLabel(label) { + _saveLabel(label, frame) { const undoLabel = this.label; const redoLabel = label; const undoAttributes = { ...this.attributes }; @@ -232,10 +232,10 @@ }, () => { this.label = redoLabel; this.attributes = redoAttributes; - }, [this.clientID]); + }, [this.clientID], frame); } - _saveAttributes(attributes) { + _saveAttributes(attributes, frame) { const undoAttributes = { ...this.attributes }; for (const attrID of Object.keys(attributes)) { @@ -248,7 +248,7 @@ this.attributes = undoAttributes; }, () => { this.attributes = redoAttributes; - }, [this.clientID]); + }, [this.clientID], frame); } _validateStateBeforeSave(frame, data, updated) { @@ -368,7 +368,7 @@ } } - delete(force) { + delete(frame, force) { if (!this.lock || force) { this.removed = true; @@ -376,7 +376,7 @@ this.removed = false; }, () => { this.removed = true; - }, [this.clientID]); + }, [this.clientID], frame); } return this.removed; @@ -392,7 +392,7 @@ this.shapeType = null; } - _savePinned(pinned) { + _savePinned(pinned, frame) { const undoPinned = this.pinned; const redoPinned = pinned; @@ -400,7 +400,7 @@ this.pinned = undoPinned; }, () => { this.pinned = redoPinned; - }, [this.clientID]); + }, [this.clientID], frame); this.pinned = pinned; } @@ -483,7 +483,7 @@ }; } - _savePoints(points) { + _savePoints(points, frame) { const undoPoints = this.points; const redoPoints = points; @@ -491,12 +491,12 @@ this.points = undoPoints; }, () => { this.points = redoPoints; - }, [this.clientID]); + }, [this.clientID], frame); this.points = points; } - _saveOccluded(occluded) { + _saveOccluded(occluded, frame) { const undoOccluded = this.occluded; const redoOccluded = occluded; @@ -504,12 +504,12 @@ this.occluded = undoOccluded; }, () => { this.occluded = redoOccluded; - }, [this.clientID]); + }, [this.clientID], frame); this.occluded = occluded; } - _saveZOrder(zOrder) { + _saveZOrder(zOrder, frame) { const undoZOrder = this.zOrder; const redoZOrder = zOrder; @@ -517,7 +517,7 @@ this.zOrder = undoZOrder; }, () => { this.zOrder = redoZOrder; - }, [this.clientID]); + }, [this.clientID], frame); this.zOrder = zOrder; } @@ -538,39 +538,39 @@ // Now when all fields are validated, we can apply them if (updated.label) { - this._saveLabel(data.label); + this._saveLabel(data.label, frame); } if (updated.attributes) { - this._saveAttributes(data.attributes); + this._saveAttributes(data.attributes, frame); } if (updated.points && fittedPoints.length) { - this._savePoints(fittedPoints); + this._savePoints(fittedPoints, frame); } if (updated.occluded) { - this._saveOccluded(data.occluded); + this._saveOccluded(data.occluded, frame); } if (updated.zOrder) { - this._saveZOrder(data.zOrder); + this._saveZOrder(data.zOrder, frame); } if (updated.lock) { - this._saveLock(data.lock); + this._saveLock(data.lock, frame); } if (updated.pinned) { - this._savePinned(data.pinned); + this._savePinned(data.pinned, frame); } if (updated.color) { - this._saveColor(data.color); + this._saveColor(data.color, frame); } if (updated.hidden) { - this._saveHidden(data.hidden); + this._saveHidden(data.hidden, frame); } this.updateTimestamp(updated); @@ -745,7 +745,7 @@ return result; } - _saveLabel(label) { + _saveLabel(label, frame) { const undoLabel = this.label; const redoLabel = label; const undoAttributes = { @@ -783,10 +783,10 @@ for (const mutable of redoAttributes.mutable) { this.shapes[mutable.frame].attributes = mutable.attributes; } - }, [this.clientID]); + }, [this.clientID], frame); } - _saveAttributes(frame, attributes) { + _saveAttributes(attributes, frame) { const current = this.get(frame); const labelAttributes = this.label.attributes .reduce((accumulator, value) => { @@ -858,7 +858,7 @@ if (redoShape) { this.shapes[frame] = redoShape; } - }, [this.clientID]); + }, [this.clientID], frame); } _appendShapeActionToHistory(actionType, frame, undoShape, redoShape) { @@ -874,10 +874,10 @@ } else { this.shapes[frame] = redoShape; } - }, [this.clientID]); + }, [this.clientID], frame); } - _savePoints(frame, points) { + _savePoints(points, frame) { const current = this.get(frame); const wasKeyframe = frame in this.shapes; const undoShape = wasKeyframe ? this.shapes[frame] : undefined; @@ -921,7 +921,7 @@ ); } - _saveOccluded(frame, occluded) { + _saveOccluded(occluded, frame) { const current = this.get(frame); const wasKeyframe = frame in this.shapes; const undoShape = wasKeyframe ? this.shapes[frame] : undefined; @@ -943,7 +943,7 @@ ); } - _saveZOrder(frame, zOrder) { + _saveZOrder(zOrder, frame) { const current = this.get(frame); const wasKeyframe = frame in this.shapes; const undoShape = wasKeyframe ? this.shapes[frame] : undefined; @@ -1007,27 +1007,27 @@ const fittedPoints = this._validateStateBeforeSave(frame, data, updated); if (updated.label) { - this._saveLabel(data.label); + this._saveLabel(data.label, frame); } if (updated.lock) { - this._saveLock(data.lock); + this._saveLock(data.lock, frame); } if (updated.pinned) { - this._savePinned(data.pinned); + this._savePinned(data.pinned, frame); } if (updated.color) { - this._saveColor(data.color); + this._saveColor(data.color, frame); } if (updated.hidden) { - this._saveHidden(data.hidden); + this._saveHidden(data.hidden, frame); } if (updated.points && fittedPoints.length) { - this._savePoints(frame, fittedPoints); + this._savePoints(fittedPoints, frame); } if (updated.outside) { @@ -1035,15 +1035,15 @@ } if (updated.occluded) { - this._saveOccluded(frame, data.occluded); + this._saveOccluded(data.occluded, frame); } if (updated.zOrder) { - this._saveZOrder(frame, data.zOrder); + this._saveZOrder(data.zOrder, frame); } if (updated.attributes) { - this._saveAttributes(frame, data.attributes); + this._saveAttributes(data.attributes, frame); } if (updated.keyframe) { @@ -1161,19 +1161,19 @@ // Now when all fields are validated, we can apply them if (updated.label) { - this._saveLabel(data.label); + this._saveLabel(data.label, frame); } if (updated.attributes) { - this._saveAttributes(data.attributes); + this._saveAttributes(data.attributes, frame); } if (updated.lock) { - this._saveLock(data.lock); + this._saveLock(data.lock, frame); } if (updated.color) { - this._saveColor(data.color); + this._saveColor(data.color, frame); } this.updateTimestamp(updated); diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js index e6a42f18..ac4e18f6 100644 --- a/cvat-core/src/object-state.js +++ b/cvat-core/src/object-state.js @@ -398,14 +398,16 @@ * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance + * @param {integer} frame current frame number * @param {boolean} [force=false] delete object even if it is locked * @async * @returns {boolean} true if object has been deleted * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} */ - async delete(force = false) { + async delete(frame, force = false) { const result = await PluginRegistry - .apiWrapper.call(this, ObjectState.prototype.delete, force); + .apiWrapper.call(this, ObjectState.prototype.delete, frame, force); return result; } } @@ -420,9 +422,13 @@ }; // Delete element from a collection which contains it - ObjectState.prototype.delete.implementation = async function (force) { + ObjectState.prototype.delete.implementation = async function (frame, force) { if (this.__internal && this.__internal.delete) { - return this.__internal.delete(force); + if (!Number.isInteger(+frame) || +frame < 0) { + throw ArgumentError('Frame argument must be a non negative integer'); + } + + return this.__internal.delete(frame, force); } return false; diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index edd5efc8..5ad15fe7 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -504,6 +504,7 @@ * @returns {HistoryActions} * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ArgumentError} + * @returns {[string, number][]} array of pairs [action name, frame number] * @instance * @async */ diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 7d35d529..02300fc9 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -290,86 +290,6 @@ export function changeAnnotationsFilters(filters: string[]): AnyAction { }; } -export function undoActionAsync(sessionInstance: any, frame: number): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - const state = getStore().getState(); - const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); - - // TODO: use affected IDs as an optimization - const [undoName] = state.annotation.annotations.history.undo.slice(-1); - const undoLog = await sessionInstance.logger.log(LogType.undoAction, { - name: undoName, - count: 1, - }, true); - await sessionInstance.actions.undo(); - const history = await sessionInstance.actions.get(); - const states = await sessionInstance.annotations - .get(frame, showAllInterpolationTracks, filters); - const [minZ, maxZ] = computeZRange(states); - await undoLog.close(); - - dispatch({ - type: AnnotationActionTypes.UNDO_ACTION_SUCCESS, - payload: { - history, - states, - minZ, - maxZ, - }, - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.UNDO_ACTION_FAILED, - payload: { - error, - }, - }); - } - }; -} - -export function redoActionAsync(sessionInstance: any, frame: number): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - const state = getStore().getState(); - const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); - - // TODO: use affected IDs as an optimization - const [redoName] = state.annotation.annotations.history.redo.slice(-1); - const redoLog = await sessionInstance.logger.log(LogType.redoAction, { - name: redoName, - count: 1, - }, true); - await sessionInstance.actions.redo(); - const history = await sessionInstance.actions.get(); - const states = await sessionInstance.annotations - .get(frame, showAllInterpolationTracks, filters); - const [minZ, maxZ] = computeZRange(states); - await redoLog.close(); - - dispatch({ - type: AnnotationActionTypes.REDO_ACTION_SUCCESS, - payload: { - history, - states, - minZ, - maxZ, - }, - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.REDO_ACTION_FAILED, - payload: { - error, - }, - }); - } - }; -} - export function updateCanvasContextMenu( visible: boolean, left: number, @@ -625,7 +545,9 @@ ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { await sessionInstance.logger.log(LogType.deleteObject, { count: 1 }); - const removed = await objectState.delete(force); + const { frame } = receiveAnnotationsParameters(); + + const removed = await objectState.delete(frame, force); const history = await sessionInstance.actions.get(); if (removed) { @@ -817,6 +739,90 @@ ThunkAction, {}, {}, AnyAction> { }; } +export function undoActionAsync(sessionInstance: any, frame: number): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + const state = getStore().getState(); + const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); + + // TODO: use affected IDs as an optimization + const [undo] = state.annotation.annotations.history.undo.slice(-1); + const undoLog = await sessionInstance.logger.log(LogType.undoAction, { + name: undo[0], + frame: undo[1], + count: 1, + }, true); + + dispatch(changeFrameAsync(undo[1])); + await sessionInstance.actions.undo(); + const history = await sessionInstance.actions.get(); + const states = await sessionInstance.annotations + .get(frame, showAllInterpolationTracks, filters); + const [minZ, maxZ] = computeZRange(states); + await undoLog.close(); + + dispatch({ + type: AnnotationActionTypes.UNDO_ACTION_SUCCESS, + payload: { + history, + states, + minZ, + maxZ, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.UNDO_ACTION_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function redoActionAsync(sessionInstance: any, frame: number): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + const state = getStore().getState(); + const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); + + // TODO: use affected IDs as an optimization + const [redo] = state.annotation.annotations.history.redo.slice(-1); + const redoLog = await sessionInstance.logger.log(LogType.redoAction, { + name: redo[0], + frame: redo[1], + count: 1, + }, true); + dispatch(changeFrameAsync(redo[1])); + await sessionInstance.actions.redo(); + const history = await sessionInstance.actions.get(); + const states = await sessionInstance.annotations + .get(frame, showAllInterpolationTracks, filters); + const [minZ, maxZ] = computeZRange(states); + await redoLog.close(); + + dispatch({ + type: AnnotationActionTypes.REDO_ACTION_SUCCESS, + payload: { + history, + states, + minZ, + maxZ, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.REDO_ACTION_FAILED, + payload: { + error, + }, + }); + } + }; +} export function rotateCurrentFrame(rotation: Rotation): AnyAction { const state: CombinedState = getStore().getState(); diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 2c8020c3..381db156 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -104,8 +104,8 @@ function mapStateToProps(state: CombinedState): StateToProps { savingStatuses, frameNumber, jobInstance, - undoAction: history.undo[history.undo.length - 1], - redoAction: history.redo[history.redo.length - 1], + undoAction: history.undo.length ? history.undo[history.undo.length - 1][0] : undefined, + redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined, autoSave, autoSaveInterval, workspace, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index f2ead557..903f43a6 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -349,8 +349,8 @@ export interface AnnotationState { filtersHistory: string[]; resetGroupFlag: boolean; history: { - undo: string[]; - redo: string[]; + undo: [string, number][]; + redo: [string, number][]; }; saving: { uploading: boolean;