Skip to content

Commit 93314fe

Browse files
Bounding Box tool improvements (#7892)
* enforce default newly create bounding boxes to be within dataset bounds * add first version bounding box moving * move bounding box by global data to be zoom independent * move bbox on ctrl / meta * add changelog entry * in case newly create bbox is outside of dataset bounds * add move hint to statusbar on ctrl pressed * use move mouse curser when in bbox tool and ctrl / meta is pressed * remove messages files * remove accidental pushed changes
1 parent d47fcff commit 93314fe

File tree

7 files changed

+133
-31
lines changed

7 files changed

+133
-31
lines changed

CHANGELOG.unreleased.md

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

1313
### Added
14+
- Added the option to move a bounding box via dragging while pressing ctrl / meta. [#7892](https://github.com/scalableminds/webknossos/pull/7892)
1415
- Added route `/import?url=<url_to_datasource>` to automatically import and view remote datasets. [#7844](https://github.com/scalableminds/webknossos/pull/7844)
1516
- The context menu that is opened upon right-clicking a segment in the dataview port now contains the segment's name. [#7920](https://github.com/scalableminds/webknossos/pull/7920)
1617

1718
### Changed
1819
- The warning about a mismatch between the scale of a pre-computed mesh and the dataset scale's factor now also considers all supported mags of the active segmentation layer. This reduces the false posive rate regarding this warning. [#7921](https://github.com/scalableminds/webknossos/pull/7921/)
1920

2021
### Fixed
22+
- Fixed a bug that allowed the default newly created bounding box to appear outside the dataset. In case the whole bounding box would be outside it is created regardless. [#7892](https://github.com/scalableminds/webknossos/pull/7892)
2123

2224
### Removed
2325

frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.ts

+42-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {
2+
calculateGlobalDelta,
23
calculateGlobalPos,
34
calculateMaybeGlobalPos,
45
} from "oxalis/model/accessors/view_mode_accessor";
56
import _ from "lodash";
67
import type { OrthoView, Point2, Vector3, BoundingBoxType, Vector2 } from "oxalis/constants";
7-
import Store from "oxalis/store";
8+
import Store, { OxalisState, UserBoundingBox } from "oxalis/store";
89
import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor";
910
import type { DimensionMap, DimensionIndices } from "oxalis/model/dimensions";
1011
import Dimension from "oxalis/model/dimensions";
@@ -241,7 +242,7 @@ export function createBoundingBoxAndGetEdges(
241242
addUserBoundingBoxAction({
242243
boundingBox: {
243244
min: globalPosition,
244-
max: V3.add(globalPosition, [1, 1, 1], [0, 0, 0]),
245+
max: V3.add(globalPosition, [1, 1, 1]),
245246
},
246247
}),
247248
);
@@ -264,16 +265,17 @@ export function createBoundingBoxAndGetEdges(
264265
}
265266

266267
export const highlightAndSetCursorOnHoveredBoundingBox = _.throttle(
267-
(position: Point2, planeId: OrthoView) => {
268+
(position: Point2, planeId: OrthoView, event: MouseEvent) => {
268269
const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, planeId);
269270
// Access the parent element as that is where the cursor style property is set
270271
const inputCatcher = document.getElementById(`inputcatcher_${planeId}`)?.parentElement;
271272

272273
if (hoveredEdgesInfo != null && inputCatcher != null) {
273274
const [primaryHoveredEdge, secondaryHoveredEdge] = hoveredEdgesInfo;
274275
getSceneController().highlightUserBoundingBox(primaryHoveredEdge.boxId);
275-
276-
if (secondaryHoveredEdge != null) {
276+
if (event.ctrlKey || event.metaKey) {
277+
inputCatcher.style.cursor = "move";
278+
} else if (secondaryHoveredEdge != null) {
277279
// If a corner is selected.
278280
inputCatcher.style.cursor =
279281
(primaryHoveredEdge.isMaxEdge && secondaryHoveredEdge.isMaxEdge) ||
@@ -295,6 +297,15 @@ export const highlightAndSetCursorOnHoveredBoundingBox = _.throttle(
295297
},
296298
BOUNDING_BOX_HOVERING_THROTTLE_TIME,
297299
);
300+
301+
function getBoundingBoxOfPrimaryEdge(
302+
primaryEdge: SelectedEdge,
303+
state: OxalisState,
304+
): UserBoundingBox | undefined {
305+
const { userBoundingBoxes } = getSomeTracing(state.tracing);
306+
return userBoundingBoxes.find((bbox) => bbox.id === primaryEdge.boxId);
307+
}
308+
298309
export function handleResizingBoundingBox(
299310
mousePosition: Point2,
300311
planeId: OrthoView,
@@ -303,8 +314,7 @@ export function handleResizingBoundingBox(
303314
) {
304315
const state = Store.getState();
305316
const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId);
306-
const { userBoundingBoxes } = getSomeTracing(state.tracing);
307-
const bboxToResize = userBoundingBoxes.find((bbox) => bbox.id === primaryEdge.boxId);
317+
const bboxToResize = getBoundingBoxOfPrimaryEdge(primaryEdge, state);
308318

309319
if (!bboxToResize) {
310320
return;
@@ -364,3 +374,28 @@ export function handleResizingBoundingBox(
364374
}),
365375
);
366376
}
377+
378+
export function handleMovingBoundingBox(
379+
delta: Point2,
380+
planeId: OrthoView,
381+
primaryEdge: SelectedEdge,
382+
) {
383+
const state = Store.getState();
384+
const globalDelta = calculateGlobalDelta(state, delta, planeId);
385+
const bboxToResize = getBoundingBoxOfPrimaryEdge(primaryEdge, state);
386+
387+
if (!bboxToResize) {
388+
return;
389+
}
390+
391+
const updatedBounds = {
392+
min: V3.toArray(V3.add(bboxToResize.boundingBox.min, globalDelta)),
393+
max: V3.toArray(V3.add(bboxToResize.boundingBox.max, globalDelta)),
394+
};
395+
396+
Store.dispatch(
397+
changeUserBoundingBoxAction(primaryEdge.boxId, {
398+
boundingBox: updatedBounds,
399+
}),
400+
);
401+
}

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

+12-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import PlaneView from "oxalis/view/plane_view";
2626
import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers";
2727
import {
2828
createBoundingBoxAndGetEdges,
29+
handleMovingBoundingBox,
2930
SelectedEdge,
3031
} from "oxalis/controller/combinations/bounding_box_handlers";
3132
import {
@@ -561,12 +562,16 @@ export class BoundingBoxTool {
561562
delta: Point2,
562563
pos: Point2,
563564
_id: string | null | undefined,
564-
_event: MouseEvent,
565+
event: MouseEvent,
565566
) => {
566-
if (primarySelectedEdge != null) {
567-
handleResizingBoundingBox(pos, planeId, primarySelectedEdge, secondarySelectedEdge);
568-
} else {
567+
if (primarySelectedEdge == null) {
569568
MoveHandlers.handleMovePlane(delta);
569+
return;
570+
}
571+
if (event.ctrlKey || event.metaKey) {
572+
handleMovingBoundingBox(delta, planeId, primarySelectedEdge);
573+
} else {
574+
handleResizingBoundingBox(pos, planeId, primarySelectedEdge, secondarySelectedEdge);
570575
}
571576
},
572577
leftMouseDown: (pos: Point2, _plane: OrthoView, _event: MouseEvent) => {
@@ -596,7 +601,7 @@ export class BoundingBoxTool {
596601
mouseMove: (delta: Point2, position: Point2, _id: any, event: MouseEvent) => {
597602
if (primarySelectedEdge == null && planeId !== OrthoViews.TDView) {
598603
MoveHandlers.moveWhenAltIsPressed(delta, position, _id, event);
599-
highlightAndSetCursorOnHoveredBoundingBox(position, planeId);
604+
highlightAndSetCursorOnHoveredBoundingBox(position, planeId, event);
600605
}
601606
},
602607
rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => {
@@ -609,12 +614,12 @@ export class BoundingBoxTool {
609614
_activeTool: AnnotationTool,
610615
_useLegacyBindings: boolean,
611616
_shiftKey: boolean,
612-
_ctrlOrMetaKey: boolean,
617+
ctrlOrMetaKey: boolean,
613618
_altKey: boolean,
614619
_isTDViewportActive: boolean,
615620
): ActionDescriptor {
616621
return {
617-
leftDrag: "Create/Resize Bounding Boxes",
622+
leftDrag: ctrlOrMetaKey ? "Move Bounding Boxes" : "Create/Resize Bounding Boxes",
618623
rightClick: "Context Menu",
619624
};
620625
}

frontend/javascripts/oxalis/model/accessors/view_mode_accessor.ts

+50
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,40 @@ function _calculateMaybePlaneScreenPos(
191191
return point;
192192
}
193193

194+
function _calculateMaybeGlobalDelta(
195+
state: OxalisState,
196+
delta: Point2,
197+
planeId?: OrthoView | null | undefined,
198+
): Vector3 | null | undefined {
199+
let position: Vector3;
200+
planeId = planeId || state.viewModeData.plane.activeViewport;
201+
const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale);
202+
const diffX = delta.x * state.flycam.zoomStep;
203+
const diffY = delta.y * state.flycam.zoomStep;
204+
205+
switch (planeId) {
206+
case OrthoViews.PLANE_XY: {
207+
position = [Math.round(diffX * planeRatio[0]), Math.round(diffY * planeRatio[1]), 0];
208+
break;
209+
}
210+
211+
case OrthoViews.PLANE_YZ: {
212+
position = [0, Math.round(diffY * planeRatio[1]), Math.round(diffX * planeRatio[2])];
213+
break;
214+
}
215+
216+
case OrthoViews.PLANE_XZ: {
217+
position = [Math.round(diffX * planeRatio[0]), 0, Math.round(diffY * planeRatio[2])];
218+
break;
219+
}
220+
221+
default:
222+
return null;
223+
}
224+
225+
return position;
226+
}
227+
194228
function _calculateGlobalPos(
195229
state: OxalisState,
196230
clickPos: Point2,
@@ -206,6 +240,21 @@ function _calculateGlobalPos(
206240
return position;
207241
}
208242

243+
function _calculateGlobalDelta(
244+
state: OxalisState,
245+
delta: Point2,
246+
planeId?: OrthoView | null | undefined,
247+
): Vector3 {
248+
const position = _calculateMaybeGlobalDelta(state, delta, planeId);
249+
250+
if (!position) {
251+
console.error("Trying to calculate the global position, but no data viewport is active.");
252+
return [0, 0, 0];
253+
}
254+
255+
return position;
256+
}
257+
209258
export function getDisplayedDataExtentInPlaneMode(state: OxalisState) {
210259
const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale);
211260
const curGlobalCenterPos = getPosition(state.flycam);
@@ -238,6 +287,7 @@ export function getDisplayedDataExtentInPlaneMode(state: OxalisState) {
238287
}
239288
export const calculateMaybeGlobalPos = reuseInstanceOnEquality(_calculateMaybeGlobalPos);
240289
export const calculateGlobalPos = reuseInstanceOnEquality(_calculateGlobalPos);
290+
export const calculateGlobalDelta = reuseInstanceOnEquality(_calculateGlobalDelta);
241291
export const calculateMaybePlaneScreenPos = reuseInstanceOnEquality(_calculateMaybePlaneScreenPos);
242292
export function getViewMode(state: OxalisState): ViewMode {
243293
return state.temporaryConfiguration.viewMode;

frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts

+7
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ class BoundingBox {
206206
const size = this.getSize();
207207
return { topLeft: this.min, width: size[0], height: size[1], depth: size[2] };
208208
}
209+
210+
toBoundingBoxType(): BoundingBoxType {
211+
return {
212+
min: this.min,
213+
max: this.max,
214+
};
215+
}
209216
}
210217

211218
export default BoundingBox;

frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import _ from "lodash";
1111
import { getAdditionalCoordinatesAsString } from "../accessors/flycam_accessor";
1212
import { getMeshesForAdditionalCoordinates } from "../accessors/volumetracing_accessor";
1313
import { AdditionalCoordinate } from "types/api_flow_types";
14+
import { getDatasetBoundingBox } from "../accessors/dataset_accessor";
15+
import BoundingBox from "../bucket_data_handling/bounding_box";
1416

1517
const updateTracing = (state: OxalisState, shape: Partial<OxalisState["tracing"]>): OxalisState =>
1618
updateKey(state, "tracing", shape);
@@ -178,17 +180,27 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState {
178180
max: V3.toArray(V3.round(V3.add(action.center, halfBoxExtent))),
179181
};
180182
}
181-
let newBoundingBox: UserBoundingBox;
183+
let newUserBoundingBox: UserBoundingBox;
182184
if (action.newBoundingBox != null) {
183-
newBoundingBox = {
185+
newUserBoundingBox = {
184186
...newBoundingBoxTemplate,
185187
...action.newBoundingBox,
186188
};
187189
} else {
188-
newBoundingBox = newBoundingBoxTemplate;
190+
newUserBoundingBox = newBoundingBoxTemplate;
189191
}
190192

191-
const updatedUserBoundingBoxes = [...userBoundingBoxes, newBoundingBox];
193+
// Ensure the new bounding box is within the dataset bounding box.
194+
const datasetBoundingBox = getDatasetBoundingBox(state.dataset);
195+
const newBoundingBox = new BoundingBox(newUserBoundingBox.boundingBox);
196+
const newBoundingBoxWithinDataset = newBoundingBox.intersectedWith(datasetBoundingBox);
197+
// Only update the bounding box if the bounding box overlaps with the dataset bounds.
198+
// Else the bounding box is completely outside the dataset bounds -> in that case just keep the bounding box and let the user cook.
199+
if (newBoundingBoxWithinDataset.getVolume() > 0) {
200+
newUserBoundingBox.boundingBox = newBoundingBoxWithinDataset.toBoundingBoxType();
201+
}
202+
203+
const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox];
192204
return updateUserBoundingBoxes(state, updatedUserBoundingBoxes);
193205
}
194206

package.json

+4-13
Original file line numberDiff line numberDiff line change
@@ -220,23 +220,14 @@
220220
"**/rc-tree": "^5.7.12"
221221
},
222222
"ava": {
223-
"files": [
224-
"./public-test/test-bundle/**/*.{js,jsx}"
225-
],
226-
"ignoredByWatcher": [
227-
"./binaryData/**/*.*"
228-
],
229-
"require": [
230-
"./frontend/javascripts/test/_ava_polyfill_provider.ts"
231-
],
223+
"files": ["./public-test/test-bundle/**/*.{js,jsx}"],
224+
"ignoredByWatcher": ["./binaryData/**/*.*"],
225+
"require": ["./frontend/javascripts/test/_ava_polyfill_provider.ts"],
232226
"snapshotDir": "frontend/javascripts/test/snapshots",
233227
"concurrency": 8
234228
},
235229
"c8": {
236-
"exclude": [
237-
"public-test/test-bundle/test/**/*.*",
238-
"frontend/javascripts/test/**/*.*"
239-
],
230+
"exclude": ["public-test/test-bundle/test/**/*.*", "frontend/javascripts/test/**/*.*"],
240231
"reporter": "lcov"
241232
}
242233
}

0 commit comments

Comments
 (0)