Skip to content

Commit 48bd317

Browse files
3D Flood fill, more robust volume editing and fix several volume bugs (#5728 + #5733)
* remove dead and unnecessary code * comment out uncalled push function * add more volume-related tests and fix two bugs * add console.error if async task queue fails * fix tests, add comments and add crash-provoking test * make dynamc overwriting of MAXIMUM_BUCKET_COUNT_PER_LAYER work in spec * fix test.sh * reference issue for bug/todo * update changelog * fix linting * fix tests * fix --timeout param for test.sh * use snapshot tests in volume tracing tests and clean up test.sh * Update tools/test.sh Co-authored-by: Daniel <[email protected]> * fix race condition in unsetting dirty flag * WIP: implement first version of flood fill (resampling is still broken) * fix z index * refactor flood fill code and fix some bugs * fix flow * fix more xyz/uvw bugs * simplify code which increments dirtyCount * limit 3d flood fill to certain bounding box, inform user whether the limit was reached, show progress for filling and further fixes for 3d flood fill * build a test which crashes undo because getData() was called on a gc-ed bucket * fix crashing undo if bucket was collected in meantime (see #5729) and add progress indicator to undo/redo * fix incorrectly mocked bucket data, properly await undo/redo and clean up * use redux channels to avoid missing critical actions, such as undo or volume-related actions, when being busy; also make undo/redo buttons AsyncClickable * clean up * fix flow * fix building docker image in CI * add more comments to applyAndGetRevertingVolumeBatch * rename action channels * rename VoxelNeighborStack3D to VoxelNeighborQueue3D * run all volume saga integration specs * clean up tests * allow to switch between 2D and 3D for fill tool * fix failing mesh loading for hybrid tracings * implement smart bbox growing for flood fill and create bbox for flood fill limit if it was reached * fix volumetracing_saga.spec * fix abort criterium in flood fill; add warning if bucket was given too small array buffer; fix off-by-one in covered floodfill bbox * fix test for bucket garbage collection * mark UI as busy when undo/redo/floodfill are computing * fix tests by clearing stale push and pull queues properly when reinitializing the model * update snapshot * fix flow * incorporate PR feedback (1/x) * incorporate more feedback * don't show progress handling if undo stack is empty and refactor corresponding code * inform user if floodfill bbox was added and show that message for 10s * update progress UI in-place, also write source_id into bbox name, don't add to undo stack if floodfill is no-op, fix flow and clean up * only show undo/redo progress message for volume actions * minor tweaks * add a comment for USE_FLOODFILL_VOXEL_THRESHOLD * intersect floodfill bbox with datasets bounding box and add snapshot test * decrease snapshot size * fix style of disabled undo/redo buttons * improve comments * make floodfill bbox message closable and deduplicate debug info code * adapt antd typing according to webknossos/pull/5738 Co-authored-by: Daniel <[email protected]>
1 parent fa3422e commit 48bd317

File tree

61 files changed

+18052
-399
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+18052
-399
lines changed

.eslintrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"no-use-before-define": ["error", { "functions": false, "classes": false }],
7373
"one-var": ["error", "never"],
7474
"operator-assignment": "warn",
75-
"prefer-destructuring": "warn",
75+
"prefer-destructuring": "off",
7676
"quote-props": ["error", "as-needed", { "numbers": true }],
7777
"quotes": ["error", "double", { "avoidEscape": true }],
7878
"radix": "off",

CHANGELOG.unreleased.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1111
[Commits](https://github.com/scalableminds/webknossos/compare/21.09.0...HEAD)
1212

1313
### Added
14-
-
14+
- Enhanced the volume fill tool to so that it operates beyond the dimensions of the current viewport. Additionally, the fill tool can also be changed to perform in 3D instead of 2D. [#5733](https://github.com/scalableminds/webknossos/pull/5733)
1515

1616
### Changed
1717
-
1818

1919
### Fixed
20+
- Fixed two volume tracing related bugs which could occur when using undo with a slow internet connection or when volume-annotating more than 5000 buckets (32**3 vx) in one session. [#5728](https://github.com/scalableminds/webknossos/pull/5728)
2021
- Jobs status is no longer polled if jobs are not enabled, avoiding backend logging spam [#5761](https://github.com/scalableminds/webknossos/pull/5761)
2122
- Fixed a bug that windows user could not open the context menu as it instantly closed after opening. [#5756](https://github.com/scalableminds/webknossos/pull/5756).
2223
- Fixed a bug where the health check of public datasets failed if no cookie/token was supplied. [#5768](https://github.com/scalableminds/webknossos/pull/5768).

flow-typed/npm/antd_vx.x.x.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
// @flow
2+
3+
type MessageConfig = {
4+
content?: string | React$Node,
5+
key?: string,
6+
duration?: number,
7+
onClose?: Function,
8+
};
9+
type MessageContent = string | React$Node | MessageConfig;
10+
111
declare module "antd" {
212
declare export class Alert<P> extends React$Component<P> {}
313
declare export class AutoComplete<P> extends React$Component<P> {
@@ -72,13 +82,15 @@ declare module "antd" {
7282
static ItemGroup: typeof MenuItemGroup;
7383
static SubMenu: typeof MenuSubMenu;
7484
}
85+
7586
declare export var message: {
76-
success(content: string | React$Node, duration?: number, onClose?: Function): Function,
77-
error(content: string | React$Node, duration?: number, onClose?: Function): Function,
78-
info(content: string | React$Node, duration?: number, onClose?: Function): Function,
79-
warning(content: string | React$Node, duration?: number, onClose?: Function): Function,
80-
warn(content: string | React$Node, duration?: number, onClose?: Function): Function,
81-
loading(content: string | React$Node, duration?: number, onClose?: Function): Function,
87+
success(content: MessageContent, duration?: number, onClose?: Function): Function,
88+
error(content: MessageContent, duration?: number, onClose?: Function): Function,
89+
info(content: MessageContent, duration?: number, onClose?: Function): Function,
90+
warning(content: MessageContent, duration?: number, onClose?: Function): Function,
91+
warn(content: MessageContent, duration?: number, onClose?: Function): Function,
92+
loading(content: MessageContent, duration?: number, onClose?: Function): Function,
93+
destroy(key: string): Function,
8294
};
8395
declare export class Modal<P> extends React$Component<P> {
8496
static confirm: Function;

frontend/javascripts/components/async_clickables.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const { useState, useEffect, useRef } = React;
66

77
type Props = {
88
onClick: (SyntheticInputEvent<>) => Promise<any>,
9+
hideContentWhenLoading?: boolean,
10+
children?: React.Node,
911
};
1012

1113
function useLoadingClickHandler(originalOnClick: (SyntheticInputEvent<>) => Promise<any>) {
@@ -38,7 +40,10 @@ function useLoadingClickHandler(originalOnClick: (SyntheticInputEvent<>) => Prom
3840

3941
export function AsyncButton(props: Props) {
4042
const [isLoading, onClick] = useLoadingClickHandler(props.onClick);
41-
return <Button {...props} loading={isLoading} onClick={onClick} />;
43+
const { children, hideContentWhenLoading, ...rest } = props;
44+
const effectiveChildren = hideContentWhenLoading && isLoading ? null : children;
45+
// eslint-disable-next-line react/no-children-prop
46+
return <Button {...rest} children={effectiveChildren} loading={isLoading} onClick={onClick} />;
4247
}
4348

4449
export function AsyncIconButton(props: Props & { icon: React.Element<*> }) {
@@ -49,7 +54,7 @@ export function AsyncIconButton(props: Props & { icon: React.Element<*> }) {
4954
});
5055
}
5156

52-
export function AsyncLink(props: Props & { children: React.Node, icon: React.Node }) {
57+
export function AsyncLink(props: Props & { icon: React.Node }) {
5358
const [isLoading, onClick] = useLoadingClickHandler(props.onClick);
5459
const icon = isLoading ? <LoadingOutlined key="loading-icon" /> : props.icon;
5560
const content = [icon, props.children];

frontend/javascripts/libs/async_task_queue.js

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class AsyncTaskQueue {
114114
this.retryCount++;
115115
this.tasks.unshift(currentTask);
116116
if (this.retryCount > this.failureEventThreshold) {
117+
console.error("AsyncTaskQueue failed with error", error);
117118
this.trigger("failure", this.retryCount);
118119
}
119120
if (this.retryCount >= this.maxRetry) {

frontend/javascripts/libs/error_handling.js

+3
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ class ErrorHandling {
159159
}
160160

161161
notify(maybeError: Object | Error, optParams: Object = {}) {
162+
if (process.env.BABEL_ENV === "test") {
163+
return;
164+
}
162165
const actionLog = getActionLog();
163166
const error = maybeError instanceof Error ? maybeError : new Error(JSON.stringify(maybeError));
164167

frontend/javascripts/libs/progress_callback.js

+25-10
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ import { sleep } from "libs/utils";
66
type HideFn = () => void;
77
export type ProgressCallback = (
88
isDone: boolean,
9-
progressState: string,
9+
progressState: string | React$Node,
1010
) => Promise<{ hideFn: HideFn }>;
1111

12+
type Options = {
13+
pauseDelay: number,
14+
successMessageDelay: number,
15+
key?: string,
16+
};
17+
1218
// This function returns another function which can be called within a longer running
1319
// process to update the UI with progress information. Example usage:
1420
// const progressCallback = createProgressCallback({ pauseDelay: 100, successMessageDelay: 5000 });
@@ -20,29 +26,38 @@ export type ProgressCallback = (
2026
//
2127
// The `progressCallback` should be awaited so that the UI can catch up
2228
// with rendering the actual feedback.
23-
export default function createProgressCallback(options: {
24-
pauseDelay: number,
25-
successMessageDelay: number,
26-
}): ProgressCallback {
27-
const { pauseDelay, successMessageDelay } = options;
29+
export default function createProgressCallback(options: Options): ProgressCallback {
2830
let hideFn = null;
29-
return async (isDone: boolean, status: string): Promise<{ hideFn: HideFn }> => {
31+
return async (
32+
isDone: boolean,
33+
status: string | React$Node,
34+
overridingOptions: $Shape<Options> = {},
35+
): Promise<{ hideFn: HideFn }> => {
3036
if (hideFn != null) {
3137
// Clear old progress message
3238
hideFn();
3339
hideFn = null;
3440
}
3541
if (!isDone) {
3642
// Show new progress message
37-
hideFn = message.loading(status, 0);
43+
hideFn = message.loading({
44+
content: status,
45+
duration: 0,
46+
key: overridingOptions.key || options.key,
47+
});
3848
// Allow the browser to catch up with rendering the progress
3949
// indicator.
50+
const pauseDelay = overridingOptions.pauseDelay || options.pauseDelay;
4051
await sleep(pauseDelay);
4152
} else {
4253
// Show success message and clear that after
4354
// ${successDelay} ms
44-
const successDelay = successMessageDelay;
45-
hideFn = message.success(status, 0);
55+
const successDelay = overridingOptions.successMessageDelay || options.successMessageDelay;
56+
hideFn = message.success({
57+
content: status,
58+
duration: 0,
59+
key: overridingOptions.key || options.key,
60+
});
4661
setTimeout(() => {
4762
if (hideFn == null) {
4863
return;

frontend/javascripts/libs/window.js

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const _window =
3434
location: dummyLocation,
3535
requestAnimationFrame: resolve => resolve(),
3636
document,
37+
navigator: {
38+
onLine: true,
39+
},
3740
}
3841
: window;
3942

frontend/javascripts/oxalis/api/api_latest.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,10 @@ class DataApi {
10101010
/**
10111011
* Invalidates all downloaded buckets so that they are reloaded.
10121012
*/
1013-
reloadAllBuckets(): void {
1013+
async reloadAllBuckets(): Promise<void> {
1014+
if (this.model.getSegmentationTracingLayer() != null) {
1015+
await Model.ensureSavedState();
1016+
}
10141017
_.forEach(this.model.dataLayers, dataLayer => {
10151018
dataLayer.cube.collectAllBuckets();
10161019
dataLayer.layerRenderingManager.refresh();
@@ -1202,7 +1205,8 @@ class DataApi {
12021205
});
12031206
}
12041207
// Bucket has been loaded by now or was loaded already
1205-
return cube.getDataValue(position, null, zoomStep);
1208+
const dataValue = cube.getDataValue(position, null, zoomStep);
1209+
return dataValue;
12061210
}
12071211

12081212
getRenderedZoomStepAtPosition(layerName: string, position: ?Vector3): number {
@@ -1385,7 +1389,6 @@ class DataApi {
13851389
}
13861390

13871391
segmentationLayer.cube.pushQueue.push();
1388-
segmentationLayer.cube.trigger("volumeLabeled");
13891392
}
13901393

13911394
/**

frontend/javascripts/oxalis/api/api_v2.js

-1
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,6 @@ class DataApi {
686686
}
687687

688688
segmentationLayer.cube.pushQueue.push();
689-
segmentationLayer.cube.trigger("volumeLabeled");
690689
}
691690

692691
/**

frontend/javascripts/oxalis/constants.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,16 @@ export const OverwriteModeEnum = {
187187
OVERWRITE_ALL: "OVERWRITE_ALL",
188188
OVERWRITE_EMPTY: "OVERWRITE_EMPTY", // In case of deleting, empty === current cell id
189189
};
190-
191190
export type OverwriteMode = $Keys<typeof OverwriteModeEnum>;
192191

192+
export const FillModeEnum = {
193+
// The leading underscore is a workaround, since leading numbers are not valid identifiers
194+
// in JS.
195+
_2D: "_2D",
196+
_3D: "_3D",
197+
};
198+
export type FillMode = $Keys<typeof FillModeEnum>;
199+
193200
export const TDViewDisplayModeEnum = {
194201
NONE: "NONE",
195202
WIREFRAME: "WIREFRAME",
@@ -219,6 +226,10 @@ export const Unicode = {
219226
// are stored in a Uint8Array in a binary way (which cell
220227
// id the voxels should be changed to is not encoded).
221228
export type LabeledVoxelsMap = Map<Vector4, Uint8Array>;
229+
// LabelMasksByBucketAndW is similar to LabeledVoxelsMap with the difference
230+
// that it can hold multiple slices per bucket (keyed by the W component,
231+
// e.g., z in XY viewport).
232+
export type LabelMasksByBucketAndW = Map<Vector4, Map<number, Uint8Array>>;
222233

223234
export type ShowContextMenuFunction = (number, number, ?number, Vector3, OrthoView) => void;
224235

@@ -265,6 +276,11 @@ const Constants = {
265276

266277
// Maximum of how many buckets will be held in RAM (per layer)
267278
MAXIMUM_BUCKET_COUNT_PER_LAYER: 5000,
279+
280+
FLOOD_FILL_EXTENTS: {
281+
_2D: process.env.BABEL_ENV === "test" ? [512, 512, 1] : [768, 768, 1],
282+
_3D: process.env.BABEL_ENV === "test" ? [64, 64, 32] : [96, 96, 96],
283+
},
268284
};
269285

270286
export default Constants;

frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.js

-22
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,11 @@ import {
3131
import ArbitraryPlane from "oxalis/geometries/arbitrary_plane";
3232
import ArbitraryView from "oxalis/view/arbitrary_view";
3333
import Crosshair from "oxalis/geometries/crosshair";
34-
import Model from "oxalis/model";
3534
import Store from "oxalis/store";
3635
import TDController from "oxalis/controller/td_controller";
3736
import Toast from "libs/toast";
3837
import * as Utils from "libs/utils";
3938
import api from "oxalis/api/internal_api";
40-
import app from "app";
4139
import constants, { ArbitraryViewport, type ViewMode, type Point2 } from "oxalis/constants";
4240
import getSceneController from "oxalis/controller/scene_controller_provider";
4341
import messages from "messages";
@@ -274,8 +272,6 @@ class ArbitraryController extends React.PureComponent<Props> {
274272
}
275273

276274
bindToEvents(): void {
277-
this.startListeningToBuckets();
278-
279275
this.storePropertyUnsubscribers.push(
280276
listenToStoreProperty(
281277
state => state.userConfiguration,
@@ -309,23 +305,6 @@ class ArbitraryController extends React.PureComponent<Props> {
309305
);
310306
}
311307

312-
onBucketLoaded = () => {
313-
this.arbitraryView.draw();
314-
app.vent.trigger("rerender");
315-
};
316-
317-
startListeningToBuckets() {
318-
for (const dataLayer of Model.getAllLayers()) {
319-
dataLayer.cube.on("bucketLoaded", this.onBucketLoaded);
320-
}
321-
}
322-
323-
stopListeningToBuckets() {
324-
for (const dataLayer of Model.getAllLayers()) {
325-
dataLayer.cube.off("bucketLoaded", this.onBucketLoaded);
326-
}
327-
}
328-
329308
start(): void {
330309
this.arbitraryView = new ArbitraryView();
331310
this.arbitraryView.start();
@@ -359,7 +338,6 @@ class ArbitraryController extends React.PureComponent<Props> {
359338
}
360339

361340
stop(): void {
362-
this.stopListeningToBuckets();
363341
this.unsubscribeStoreListeners();
364342

365343
if (this.isStarted) {

frontend/javascripts/oxalis/default_state.js

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Constants, {
55
ControlModeEnum,
66
OrthoViews,
77
OverwriteModeEnum,
8+
FillModeEnum,
89
TDViewDisplayModeEnum,
910
} from "oxalis/constants";
1011
import { document } from "libs/window";
@@ -80,6 +81,7 @@ const defaultState: OxalisState = {
8081
tdViewDisplayDatasetBorders: true,
8182
gpuMemoryFactor: Constants.DEFAULT_GPU_MEMORY_FACTOR,
8283
overwriteMode: OverwriteModeEnum.OVERWRITE_ALL,
84+
fillMode: FillModeEnum._2D,
8385
useLegacyBindings: false,
8486
},
8587
temporaryConfiguration: {
@@ -209,6 +211,9 @@ const defaultState: OxalisState = {
209211
primaryStylesheetElement != null && primaryStylesheetElement.href.includes("dark.css")
210212
? "dark"
211213
: "light",
214+
busyBlockingInfo: {
215+
isBusy: false,
216+
},
212217
},
213218
isosurfacesByLayer: {},
214219
currentMeshFileByLayer: {},

frontend/javascripts/oxalis/model.js

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export class OxalisModel {
5959
isMappingSupported,
6060
maximumTextureCountForLayer,
6161
} = initializationInformation;
62+
if (this.dataLayers != null) {
63+
_.values(this.dataLayers).forEach(layer => layer.destroy());
64+
}
6265
this.dataLayers = dataLayers;
6366
this.connectionInfo = connectionInfo;
6467
this.isMappingSupported = isMappingSupported;

frontend/javascripts/oxalis/model/accessors/dataset_accessor.js

+4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export class ResolutionInfo {
7070
}
7171
}
7272

73+
getDenseResolutions(): Array<Vector3> {
74+
return convertToDenseResolution(this.getResolutionList());
75+
}
76+
7377
getResolutionList(): Array<Vector3> {
7478
return Array.from(this.resolutionMap.entries()).map(entry => entry[1]);
7579
}

0 commit comments

Comments
 (0)