Skip to content

Commit 9b12883

Browse files
Add multi click split tool for proofreading (#8824)
### URL of deployed dev instance (used for testing): - https://multisplittool.webknossos.xyz/ ### Steps to test: - Open a dataset with an agglomerate file and create an annotation - Activate a mapping and the proofreading tool. - Load a mesh which you want to split for testing purposes. - try pressing m multiple times. I should toggle the multi cut tool. Visible via a toggle on the right of the toolbar. Should also be toggable via manual clicking. - Turn on the multi cut tool. Start marking super voxels of the loaded mesh with ctrl +click / shift + ctrl + click to mark 2 paritions. Confirm the cut by pressing enter. Also try confirm the cut and mesh paritioning selection via the context menu in the 3d viewport - Try mesh paritioning selection in teh ortho views via the same shortcuts or context menu. Should also work. - Once split, the two new meshes should auto reload and look accurate according to the previously marked partitions - That should be it :) ### TODOs: - [x] Change color partitioning to light & dark gray. Alternatively, partition one: brighter variant of the mesh color; partition two darker variant of the mesh color. - [x] Hitting `enter` upon partition select should do the cut (context menu interaction is not needed) - [x] Enable segment selection in ortho viewports - [x] Shortcut to enter / leave multi cut tool when proofreading is active (e.g. `m` / `c`) - [x] Shortcut to perform cut via `enter` and cancel selection via `Escape` ### Issues: - fixes #8690 ------ (Please delete unneeded items, merge only when none are left open) - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [x] Updated [documentation](../blob/master/docs) if applicable --------- Co-authored-by: Florian M <fm3@users.noreply.github.com>
1 parent d06f425 commit 9b12883

File tree

20 files changed

+970
-236
lines changed

20 files changed

+970
-236
lines changed

docs/ui/keyboard_shortcuts.md

Lines changed: 145 additions & 109 deletions
Large diffs are not rendered by default.

frontend/javascripts/admin/rest_api.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,8 +2225,8 @@ export async function getEdgesForAgglomerateMinCut(
22252225
tracingStoreUrl: string,
22262226
tracingId: string,
22272227
segmentsInfo: {
2228-
segmentId1: NumberLike;
2229-
segmentId2: NumberLike;
2228+
partition1: NumberLike[];
2229+
partition2: NumberLike[];
22302230
mag: Vector3;
22312231
agglomerateId: NumberLike;
22322232
editableMappingId: string;
@@ -2240,8 +2240,8 @@ export async function getEdgesForAgglomerateMinCut(
22402240
data: {
22412241
...segmentsInfo,
22422242
// TODO: Proper 64 bit support (#6921)
2243-
segmentId1: Number(segmentsInfo.segmentId1),
2244-
segmentId2: Number(segmentsInfo.segmentId2),
2243+
partition1: segmentsInfo.partition1.map(Number),
2244+
partition2: segmentsInfo.partition2.map(Number),
22452245
agglomerateId: Number(segmentsInfo.agglomerateId),
22462246
},
22472247
},

frontend/javascripts/messages.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,12 @@ instead. Only enable this option if you understand its effect. All layers will n
311311
"The annotation was successfully unlocked. Reloading this annotation ...",
312312
"annotation.lock.success":
313313
"The annotation was successfully locked. Reloading this annotation ...",
314+
"proofreading.multi_cut.different_agglomerate_selected": `The selected segment belongs to a different agglomerate than the one that is currently partitioned via selections. If you want to split this other agglomerate please clear your current selection with "ESC" first.`,
315+
"proofreading.multi_cut.empty_partition":
316+
"Not every partition has at least one selected segment. Select at least one segment for each partition before performing a cut action.",
317+
"proofreading.multi_cut.no_valid_agglomerate":
318+
"No agglomerate for the selected segments could be found. Please retry with a new selection.",
319+
"proofreading.multi_cut.split_failed": "Could not determine a valid split. Operation failed.",
314320
"task.bulk_create_invalid":
315321
"Can not parse task specification. It includes at least one invalid task.",
316322
"task.recommended_configuration": "The author of this task suggests to use these settings:",

frontend/javascripts/viewer/controller.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { initializeSceneController } from "viewer/controller/scene_controller";
2121
import UrlManager from "viewer/controller/url_manager";
2222
import ArbitraryController from "viewer/controller/viewmodes/arbitrary_controller";
2323
import PlaneController from "viewer/controller/viewmodes/plane_controller";
24+
import { AnnotationTool } from "viewer/model/accessors/tool_accessor";
2425
import { wkReadyAction } from "viewer/model/actions/actions";
2526
import { redoAction, saveNowAction, undoAction } from "viewer/model/actions/save_actions";
2627
import { setViewModeAction, updateLayerSettingAction } from "viewer/model/actions/settings_actions";
@@ -232,8 +233,14 @@ class Controller extends React.PureComponent<PropsWithRouter, State> {
232233
"shift + 3": () => Store.dispatch(setViewModeAction(constants.MODE_ARBITRARY_PLANE)),
233234
m: () => {
234235
// rotate allowed modes
235-
const currentViewMode = Store.getState().temporaryConfiguration.viewMode;
236-
const { allowedModes } = Store.getState().annotation.restrictions;
236+
const state = Store.getState();
237+
const isProofreadingActive = state.uiInformation.activeTool === AnnotationTool.PROOFREAD;
238+
const currentViewMode = state.temporaryConfiguration.viewMode;
239+
if (isProofreadingActive && currentViewMode === constants.MODE_PLANE_TRACING) {
240+
// Skipping cycling view mode as m in proofreading is used to toggle multi cut tool.
241+
return;
242+
}
243+
const { allowedModes } = state.annotation.restrictions;
237244
const index = (allowedModes.indexOf(currentViewMode) + 1) % allowedModes.length;
238245
Store.dispatch(setViewModeAction(allowedModes[index]));
239246
},

frontend/javascripts/viewer/controller/combinations/tool_controls.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
minCutAgglomerateWithPositionAction,
4545
proofreadAtPosition,
4646
proofreadMergeAction,
47+
toggleSegmentInPartitionAction,
4748
} from "viewer/model/actions/proofread_actions";
4849
import {
4950
hideMeasurementTooltipAction,
@@ -1080,6 +1081,19 @@ export class ProofreadToolController {
10801081

10811082
const state = Store.getState();
10821083
const globalPosition = calculateGlobalPos(state, pos).rounded;
1084+
const isMultiSplitActive = state.userConfiguration.isMultiSplitActive;
1085+
const ctrlOrMetaKey = event.ctrlKey || event.metaKey;
1086+
if (isMultiSplitActive && ctrlOrMetaKey) {
1087+
const unmappedSegmentId = VolumeHandlers.getUnmappedSegmentIdForPosition(globalPosition);
1088+
const mappedSegmentId = VolumeHandlers.getSegmentIdForPosition(globalPosition);
1089+
if (unmappedSegmentId === 0 || mappedSegmentId === 0) {
1090+
// No valid ids were found, ignore action.
1091+
return;
1092+
}
1093+
const partition = event.shiftKey ? 2 : 1;
1094+
Store.dispatch(toggleSegmentInPartitionAction(unmappedSegmentId, partition, mappedSegmentId));
1095+
return;
1096+
}
10831097

10841098
if (event.shiftKey) {
10851099
Store.dispatch(proofreadMergeAction(globalPosition));
@@ -1095,30 +1109,42 @@ export class ProofreadToolController {
10951109

10961110
static getActionDescriptors(
10971111
_activeTool: AnnotationTool,
1098-
_userConfiguration: UserConfiguration,
1112+
userConfiguration: UserConfiguration,
10991113
shiftKey: boolean,
11001114
ctrlOrMetaKey: boolean,
11011115
_altKey: boolean,
11021116
isTDViewportActive: boolean,
11031117
): ActionDescriptor {
1118+
const { isMultiSplitActive } = userConfiguration;
1119+
1120+
// --- Multi-split additions -----------------------------------------------
1121+
const multiSplitOverwrites: Partial<ActionDescriptor> = {};
1122+
if (isMultiSplitActive) {
1123+
if (shiftKey && ctrlOrMetaKey) {
1124+
multiSplitOverwrites.leftClick = "Add to Partition 2";
1125+
} else if (ctrlOrMetaKey) {
1126+
multiSplitOverwrites.leftClick = "Add to Partition 1";
1127+
}
1128+
}
1129+
1130+
// --- TD-viewport viewport -----------------------------------------------------
11041131
if (isTDViewportActive) {
1105-
let maybeLeftClick = {};
1132+
let maybeLeftClick: Partial<ActionDescriptor> = {};
11061133
if (shiftKey) {
1107-
maybeLeftClick = {
1108-
leftClick: "Jump to point",
1109-
};
1134+
maybeLeftClick.leftClick = "Jump to point";
11101135
} else if (ctrlOrMetaKey) {
1111-
maybeLeftClick = {
1112-
leftClick: "Activate super-voxel",
1113-
};
1136+
maybeLeftClick.leftClick = "Activate super-voxel";
11141137
}
11151138

11161139
return {
11171140
...maybeLeftClick,
1141+
...multiSplitOverwrites,
11181142
leftDrag: "Move",
11191143
rightClick: "Context Menu",
11201144
};
11211145
}
1146+
1147+
// --- Default ortho viewports -------------------------------------------
11221148
let leftClick = "Select Segment to Proofread";
11231149
if (shiftKey) {
11241150
leftClick = "Merge with active Segment";
@@ -1128,6 +1154,7 @@ export class ProofreadToolController {
11281154

11291155
return {
11301156
leftClick,
1157+
...multiSplitOverwrites,
11311158
rightClick: "Context Menu",
11321159
};
11331160
}

frontend/javascripts/viewer/controller/combinations/volume_handlers.ts

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,49 @@ export function handlePickCell(pos: Point2) {
4747
storeState.flycam.additionalCoordinates || [],
4848
);
4949
}
50-
export const getSegmentIdForPosition = memoizeOne(
51-
(globalPos: Vector3) => {
52-
// This function will return the currently loaded segment ID for a given position.
53-
// If the corresponding bucket is not loaded at the moment, the return value will be 0.
54-
// See getSegmentIdForPositionAsync if the bucket loading should be awaited before returning the ID.
55-
const layer = Model.getVisibleSegmentationLayer();
56-
const { additionalCoordinates } = Store.getState().flycam;
5750

58-
if (!layer) {
59-
return 0;
60-
}
61-
const posInLayerSpace = globalToLayerTransformedPosition(
62-
globalPos,
63-
layer.name,
64-
"segmentation",
65-
Store.getState(),
66-
);
51+
const _getSegmentIdForPosition = (mapped: boolean) => (globalPos: Vector3) => {
52+
// This function will return the currently loaded segment ID for a given position.
53+
// If the corresponding bucket is not loaded at the moment, the return value will be 0.
54+
// See getSegmentIdForPositionAsync if the bucket loading should be awaited before returning the ID.
55+
const layer = Model.getVisibleSegmentationLayer();
56+
const { additionalCoordinates } = Store.getState().flycam;
6757

68-
const segmentationCube = layer.cube;
69-
const segmentationLayerName = layer.name;
70-
const renderedZoomStepForCameraPosition = api.data.getRenderedZoomStepAtPosition(
71-
segmentationLayerName,
72-
posInLayerSpace,
73-
);
74-
return segmentationCube.getMappedDataValue(
75-
posInLayerSpace,
76-
additionalCoordinates,
77-
renderedZoomStepForCameraPosition,
78-
);
79-
},
58+
if (!layer) {
59+
return 0;
60+
}
61+
const posInLayerSpace = globalToLayerTransformedPosition(
62+
globalPos,
63+
layer.name,
64+
"segmentation",
65+
Store.getState(),
66+
);
67+
68+
const segmentationCube = layer.cube;
69+
const segmentationLayerName = layer.name;
70+
const renderedZoomStepForCameraPosition = api.data.getRenderedZoomStepAtPosition(
71+
segmentationLayerName,
72+
posInLayerSpace,
73+
);
74+
75+
return mapped
76+
? segmentationCube.getMappedDataValue(
77+
posInLayerSpace,
78+
additionalCoordinates,
79+
renderedZoomStepForCameraPosition,
80+
)
81+
: segmentationCube.getDataValue(
82+
posInLayerSpace,
83+
additionalCoordinates,
84+
null,
85+
renderedZoomStepForCameraPosition,
86+
);
87+
};
88+
export const getSegmentIdForPosition = memoizeOne(_getSegmentIdForPosition(true), ([a], [b]) =>
89+
V3.isEqual(a, b),
90+
);
91+
export const getUnmappedSegmentIdForPosition = memoizeOne(
92+
_getSegmentIdForPosition(false),
8093
([a], [b]) => V3.isEqual(a, b),
8194
);
8295
export async function getSegmentIdForPositionAsync(globalPos: Vector3) {

0 commit comments

Comments
 (0)