Skip to content

Commit

Permalink
Time-track movements in the 3D viewport (#4876)
Browse files Browse the repository at this point in the history
* add updateTdCamera update action, improve version view labeling and add many missing actions, improve save queue compaction

* remove console.log

* fix unit tests

* add updatetdcamera action in backend

* introduce some td view action duplicates that do not lead to save queue diffing

* update changelog

Co-authored-by: Florian M <[email protected]>
  • Loading branch information
daniel-wer and fm3 authored Oct 20, 2020
1 parent 40c3598 commit e9982a0
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 152 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- The find data function now works for volume tracings, too. [#4847](https://github.com/scalableminds/webknossos/pull/4847)
- Added admins and dataset managers to dataset access list, as they can access all datasets of the organization. [#4862](https://github.com/scalableminds/webknossos/pull/4862)
- Sped up the NML parsing via dashboard import. [#4872](https://github.com/scalableminds/webknossos/pull/4872)
- Movements in the 3D viewport are now time-tracked. [#4876](https://github.com/scalableminds/webknossos/pull/4876)

### Changed
- Brush circles are now connected with rectangles to provide a continuous stroke even if the brush is moved quickly. [#4785](https://github.com/scalableminds/webknossos/pull/4822)
Expand Down
8 changes: 4 additions & 4 deletions frontend/javascripts/libs/trackball_controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function TrackballControls(object, domElement, target, updateCallback) {
}
};

this.update = (externalUpdate = false) => {
this.update = (externalUpdate = false, userTriggered = false) => {
_eye.subVectors(_this.object.position, _this.lastTarget);

if (!_this.noRotate) {
Expand Down Expand Up @@ -298,7 +298,7 @@ function TrackballControls(object, domElement, target, updateCallback) {

_this.lastTarget = _this.target.clone();
if (!externalUpdate) {
_this.updateCallback();
_this.updateCallback(userTriggered);
}
};

Expand Down Expand Up @@ -383,7 +383,7 @@ function TrackballControls(object, domElement, target, updateCallback) {
} else if (_state === STATE.PAN && !_this.noPan) {
_this.getMouseOnScreen(event.pageX, event.pageY, _panEnd);
}
_this.update();
_this.update(false, true);
}

function mouseup(event) {
Expand Down Expand Up @@ -490,7 +490,7 @@ function TrackballControls(object, domElement, target, updateCallback) {
default:
_state = STATE.NONE;
}
_this.update();
_this.update(false, true);
}

function touchend(event) {
Expand Down
6 changes: 3 additions & 3 deletions frontend/javascripts/oxalis/controller/camera_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
getPosition,
} from "oxalis/model/accessors/flycam_accessor";
import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers";
import { setTDCameraAction } from "oxalis/model/actions/view_mode_actions";
import { setTDCameraWithoutTimeTrackingAction } from "oxalis/model/actions/view_mode_actions";
import { voxelToNm, getBaseVoxel } from "oxalis/model/scaleinfo";
import Store, { type CameraData } from "oxalis/store";
import api from "oxalis/api/internal_api";
Expand Down Expand Up @@ -94,7 +94,7 @@ class CameraController extends React.PureComponent<Props> {
}

Store.dispatch(
setTDCameraAction({
setTDCameraWithoutTimeTrackingAction({
near: 0,
far,
}),
Expand Down Expand Up @@ -324,7 +324,7 @@ export function rotate3DViewTo(id: OrthoView, animate: boolean = true): void {
);

Store.dispatch(
setTDCameraAction({
setTDCameraWithoutTimeTrackingAction({
position: newPosition,
up: tweened.up,
left,
Expand Down
21 changes: 15 additions & 6 deletions frontend/javascripts/oxalis/controller/td_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import { setPositionAction } from "oxalis/model/actions/flycam_actions";
import {
setViewportAction,
setTDCameraAction,
setTDCameraWithoutTimeTrackingAction,
zoomTDViewAction,
moveTDViewXAction,
moveTDViewYAction,
moveTDViewByVectorAction,
moveTDViewByVectorWithoutTimeTrackingAction,
} from "oxalis/model/actions/view_mode_actions";
import { getActiveNode } from "oxalis/model/accessors/skeletontracing_accessor";
import { voxelToNm } from "oxalis/model/scaleinfo";
Expand Down Expand Up @@ -124,10 +125,18 @@ class TDController extends React.PureComponent<Props> {
initTrackballControls(view: HTMLElement): void {
const pos = voxelToNm(this.props.scale, getPosition(this.props.flycam));
const tdCamera = this.props.cameras[OrthoViews.TDView];
this.controls = new TrackballControls(tdCamera, view, new THREE.Vector3(...pos), () => {
// write threeJS camera into store
Store.dispatch(setTDCameraAction(threeCameraToCameraData(tdCamera)));
});
this.controls = new TrackballControls(
tdCamera,
view,
new THREE.Vector3(...pos),
(userTriggered: boolean = true) => {
const setCameraAction = userTriggered
? setTDCameraAction
: setTDCameraWithoutTimeTrackingAction;
// write threeJS camera into store
Store.dispatch(setCameraAction(threeCameraToCameraData(tdCamera)));
},
);

this.controls.noZoom = true;
this.controls.noPan = true;
Expand Down Expand Up @@ -225,7 +234,7 @@ class TDController extends React.PureComponent<Props> {

nmVector.applyEuler(rotation);

Store.dispatch(moveTDViewByVectorAction(nmVector.x, nmVector.y));
Store.dispatch(moveTDViewByVectorWithoutTimeTrackingAction(nmVector.x, nmVector.y));
}

zoomTDView(value: number, zoomToMouse: boolean = true): void {
Expand Down
44 changes: 44 additions & 0 deletions frontend/javascripts/oxalis/model/actions/view_mode_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ type MoveTDViewByVectorAction = {
y: number,
};

// These two actions are used instead of their functionally identical counterparts
// (without the `_WITHOUT_TIME_TRACKING` suffix)
// when dispatching these actions should not trigger the save queue diffing.
// Therefore, the save queue will not become dirty and no time is tracked by the backend.
// The actions are used by initialization code and by the `setTargetAndFixPosition`
// workaround in the td_controller.js.
type SetTDCameraWithoutTimeTrackingAction = {
type: "SET_TD_CAMERA_WITHOUT_TIME_TRACKING",
cameraData: PartialCameraData,
};

type MoveTDViewByVectorWithoutTimeTrackingAction = {
type: "MOVE_TD_VIEW_BY_VECTOR_WITHOUT_TIME_TRACKING",
x: number,
y: number,
};

type SetInputCatcherRect = {
type: "SET_INPUT_CATCHER_RECT",
viewport: Viewport,
Expand All @@ -60,6 +77,14 @@ export const setTDCameraAction = (cameraData: PartialCameraData): SetTDCameraAct
cameraData,
});

// See the explanation further up for when to use this action instead of the setTDCameraAction
export const setTDCameraWithoutTimeTrackingAction = (
cameraData: PartialCameraData,
): SetTDCameraWithoutTimeTrackingAction => ({
type: "SET_TD_CAMERA_WITHOUT_TIME_TRACKING",
cameraData,
});

export const centerTDViewAction = (): CenterTDViewAction => ({
type: "CENTER_TD_VIEW",
});
Expand All @@ -83,6 +108,16 @@ export const moveTDViewByVectorAction = (x: number, y: number): MoveTDViewByVect
y,
});

// See the explanation further up for when to use this action instead of the moveTDViewByVectorAction
export const moveTDViewByVectorWithoutTimeTrackingAction = (
x: number,
y: number,
): MoveTDViewByVectorWithoutTimeTrackingAction => ({
type: "MOVE_TD_VIEW_BY_VECTOR_WITHOUT_TIME_TRACKING",
x,
y,
});

export const moveTDViewXAction = (x: number): MoveTDViewByVectorAction => {
const state = Store.getState();
return moveTDViewByVectorAction((x * getTDViewportSize(state)[0]) / constants.VIEWPORT_WIDTH, 0);
Expand All @@ -107,10 +142,19 @@ export const setInputCatcherRects = (viewportRects: ViewportRects): SetInputCatc
export type ViewModeAction =
| SetViewportAction
| SetTDCameraAction
| SetTDCameraWithoutTimeTrackingAction
| CenterTDViewAction
| ZoomTDViewAction
| MoveTDViewByVectorAction
| MoveTDViewByVectorWithoutTimeTrackingAction
| SetInputCatcherRect
| SetInputCatcherRects;

export const ViewModeSaveRelevantActions = [
"SET_TD_CAMERA",
"CENTER_TD_VIEW",
"ZOOM_TD_VIEW",
"MOVE_TD_VIEW_BY_VECTOR",
];

export default {};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ function removeAllButLastUpdateTracingAction(updateActionsBatches: Array<SaveQue
return _.without(updateActionsBatches, ...updateTracingOnlyBatches.slice(0, -1));
}

function removeAllButLastUpdateTdCameraAction(updateActionsBatches: Array<SaveQueueEntry>) {
// This part of the code removes all entries from the save queue that consist only of
// one updateTdCamera update action, except for the last one
const updateTracingOnlyBatches = updateActionsBatches.filter(
batch => batch.actions.length === 1 && batch.actions[0].name === "updateTdCamera",
);
return _.without(updateActionsBatches, ...updateTracingOnlyBatches.slice(0, -1));
}

function removeSubsequentUpdateTreeActions(updateActionsBatches: Array<SaveQueueEntry>) {
const obsoleteUpdateActions = [];
// If two updateTree update actions for the same treeId follow one another, the first one is obsolete
Expand All @@ -32,6 +41,25 @@ function removeSubsequentUpdateTreeActions(updateActionsBatches: Array<SaveQueue
return _.without(updateActionsBatches, ...obsoleteUpdateActions);
}

function removeSubsequentUpdateNodeActions(updateActionsBatches: Array<SaveQueueEntry>) {
const obsoleteUpdateActions = [];
// If two updateNode update actions for the same nodeId follow one another, the first one is obsolete
for (let i = 0; i < updateActionsBatches.length - 1; i++) {
const actions1 = updateActionsBatches[i].actions;
const actions2 = updateActionsBatches[i + 1].actions;
if (
actions1.length === 1 &&
actions1[0].name === "updateNode" &&
actions2.length === 1 &&
actions2[0].name === "updateNode" &&
actions1[0].value.id === actions2[0].value.id
) {
obsoleteUpdateActions.push(updateActionsBatches[i]);
}
}
return _.without(updateActionsBatches, ...obsoleteUpdateActions);
}

export default function compactSaveQueue(
updateActionsBatches: Array<SaveQueueEntry>,
): Array<SaveQueueEntry> {
Expand All @@ -40,5 +68,9 @@ export default function compactSaveQueue(
updateActionsBatch => updateActionsBatch.actions.length > 0,
);

return removeSubsequentUpdateTreeActions(removeAllButLastUpdateTracingAction(result));
return removeSubsequentUpdateTreeActions(
removeSubsequentUpdateNodeActions(
removeAllButLastUpdateTdCameraAction(removeAllButLastUpdateTracingAction(result)),
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function ViewModeReducer(state: OxalisState, action: Action): OxalisState {
},
});
}
case "SET_TD_CAMERA_WITHOUT_TIME_TRACKING":
case "SET_TD_CAMERA": {
return setTDCameraReducer(state, action.cameraData);
}
Expand All @@ -35,6 +36,7 @@ function ViewModeReducer(state: OxalisState, action: Action): OxalisState {
action.curHeight,
);
}
case "MOVE_TD_VIEW_BY_VECTOR_WITHOUT_TIME_TRACKING":
case "MOVE_TD_VIEW_BY_VECTOR": {
return moveTDViewByVectorReducer(state, action.x, action.y);
}
Expand Down
38 changes: 33 additions & 5 deletions frontend/javascripts/oxalis/model/sagas/save_saga.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
UNDO_HISTORY_SIZE,
maximumActionCountPerSave,
} 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 type { Tracing, SkeletonTracing, Flycam, SaveQueueEntry, CameraData } from "oxalis/store";
import { type UpdateAction, updateTdCamera } from "oxalis/model/sagas/update_actions";
import {
VolumeTracingSaveRelevantActions,
type AddBucketToUndoAction,
Expand All @@ -41,6 +41,7 @@ import {
setTracingAction,
centerActiveNodeAction,
} from "oxalis/model/actions/skeletontracing_actions";
import { ViewModeSaveRelevantActions } from "oxalis/model/actions/view_mode_actions";
import type { Action } from "oxalis/model/actions/actions";
import { diffSkeletonTracing } from "oxalis/model/sagas/skeletontracing_saga";
import { diffVolumeTracing } from "oxalis/model/sagas/volumetracing_saga";
Expand Down Expand Up @@ -460,6 +461,8 @@ export function performDiffTracing(
tracing: Tracing,
prevFlycam: Flycam,
flycam: Flycam,
prevTdCamera: CameraData,
tdCamera: CameraData,
): Array<UpdateAction> {
let actions = [];
if (tracingType === "skeleton" && tracing.skeleton != null && prevTracing.skeleton != null) {
Expand All @@ -474,6 +477,10 @@ export function performDiffTracing(
);
}

if (prevTdCamera !== tdCamera) {
actions = actions.concat(updateTdCamera());
}

return actions;
}

Expand All @@ -488,6 +495,7 @@ export function* saveTracingTypeAsync(tracingType: "skeleton" | "volume"): Saga<

let prevTracing = yield* select(state => state.tracing);
let prevFlycam = yield* select(state => state.flycam);
let prevTdCamera = yield* select(state => state.viewModeData.plane.tdCamera);

yield* take("WK_READY");
const initialAllowUpdate = yield* select(
Expand All @@ -500,9 +508,18 @@ export function* saveTracingTypeAsync(tracingType: "skeleton" | "volume"): Saga<

while (true) {
if (tracingType === "skeleton") {
yield* take([...SkeletonTracingSaveRelevantActions, ...FlycamActions, "SET_TRACING"]);
yield* take([
...SkeletonTracingSaveRelevantActions,
...FlycamActions,
...ViewModeSaveRelevantActions,
"SET_TRACING",
]);
} else {
yield* take([...VolumeTracingSaveRelevantActions, ...FlycamActions]);
yield* take([
...VolumeTracingSaveRelevantActions,
...FlycamActions,
...ViewModeSaveRelevantActions,
]);
}
// The allowUpdate setting could have changed in the meantime
const allowUpdate = yield* select(
Expand All @@ -515,10 +532,20 @@ export function* saveTracingTypeAsync(tracingType: "skeleton" | "volume"): Saga<

const tracing = yield* select(state => state.tracing);
const flycam = yield* select(state => state.flycam);
const tdCamera = yield* select(state => state.viewModeData.plane.tdCamera);
const items = compactUpdateActions(
// $FlowFixMe[incompatible-call] Should be resolved when we improve the typing of sagas in general
Array.from(
yield* call(performDiffTracing, tracingType, prevTracing, tracing, prevFlycam, flycam),
yield* call(
performDiffTracing,
tracingType,
prevTracing,
tracing,
prevFlycam,
flycam,
prevTdCamera,
tdCamera,
),
),
tracing,
);
Expand All @@ -527,5 +554,6 @@ export function* saveTracingTypeAsync(tracingType: "skeleton" | "volume"): Saga<
}
prevTracing = tracing;
prevFlycam = flycam;
prevTdCamera = tdCamera;
}
}
Loading

0 comments on commit e9982a0

Please sign in to comment.