Skip to content

Commit

Permalink
Bounding Box tool improvements (#7892)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MichaelBuessemeyer authored Jul 17, 2024
1 parent d47fcff commit 93314fe
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/24.07.0...HEAD)

### Added
- Added the option to move a bounding box via dragging while pressing ctrl / meta. [#7892](https://github.com/scalableminds/webknossos/pull/7892)
- Added route `/import?url=<url_to_datasource>` to automatically import and view remote datasets. [#7844](https://github.com/scalableminds/webknossos/pull/7844)
- 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)

### Changed
- 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/)

### Fixed
- 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)

### Removed

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
calculateGlobalDelta,
calculateGlobalPos,
calculateMaybeGlobalPos,
} from "oxalis/model/accessors/view_mode_accessor";
import _ from "lodash";
import type { OrthoView, Point2, Vector3, BoundingBoxType, Vector2 } from "oxalis/constants";
import Store from "oxalis/store";
import Store, { OxalisState, UserBoundingBox } from "oxalis/store";
import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor";
import type { DimensionMap, DimensionIndices } from "oxalis/model/dimensions";
import Dimension from "oxalis/model/dimensions";
Expand Down Expand Up @@ -241,7 +242,7 @@ export function createBoundingBoxAndGetEdges(
addUserBoundingBoxAction({
boundingBox: {
min: globalPosition,
max: V3.add(globalPosition, [1, 1, 1], [0, 0, 0]),
max: V3.add(globalPosition, [1, 1, 1]),
},
}),
);
Expand All @@ -264,16 +265,17 @@ export function createBoundingBoxAndGetEdges(
}

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

if (hoveredEdgesInfo != null && inputCatcher != null) {
const [primaryHoveredEdge, secondaryHoveredEdge] = hoveredEdgesInfo;
getSceneController().highlightUserBoundingBox(primaryHoveredEdge.boxId);

if (secondaryHoveredEdge != null) {
if (event.ctrlKey || event.metaKey) {
inputCatcher.style.cursor = "move";
} else if (secondaryHoveredEdge != null) {
// If a corner is selected.
inputCatcher.style.cursor =
(primaryHoveredEdge.isMaxEdge && secondaryHoveredEdge.isMaxEdge) ||
Expand All @@ -295,6 +297,15 @@ export const highlightAndSetCursorOnHoveredBoundingBox = _.throttle(
},
BOUNDING_BOX_HOVERING_THROTTLE_TIME,
);

function getBoundingBoxOfPrimaryEdge(
primaryEdge: SelectedEdge,
state: OxalisState,
): UserBoundingBox | undefined {
const { userBoundingBoxes } = getSomeTracing(state.tracing);
return userBoundingBoxes.find((bbox) => bbox.id === primaryEdge.boxId);
}

export function handleResizingBoundingBox(
mousePosition: Point2,
planeId: OrthoView,
Expand All @@ -303,8 +314,7 @@ export function handleResizingBoundingBox(
) {
const state = Store.getState();
const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId);
const { userBoundingBoxes } = getSomeTracing(state.tracing);
const bboxToResize = userBoundingBoxes.find((bbox) => bbox.id === primaryEdge.boxId);
const bboxToResize = getBoundingBoxOfPrimaryEdge(primaryEdge, state);

if (!bboxToResize) {
return;
Expand Down Expand Up @@ -364,3 +374,28 @@ export function handleResizingBoundingBox(
}),
);
}

export function handleMovingBoundingBox(
delta: Point2,
planeId: OrthoView,
primaryEdge: SelectedEdge,
) {
const state = Store.getState();
const globalDelta = calculateGlobalDelta(state, delta, planeId);
const bboxToResize = getBoundingBoxOfPrimaryEdge(primaryEdge, state);

if (!bboxToResize) {
return;
}

const updatedBounds = {
min: V3.toArray(V3.add(bboxToResize.boundingBox.min, globalDelta)),
max: V3.toArray(V3.add(bboxToResize.boundingBox.max, globalDelta)),
};

Store.dispatch(
changeUserBoundingBoxAction(primaryEdge.boxId, {
boundingBox: updatedBounds,
}),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import PlaneView from "oxalis/view/plane_view";
import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers";
import {
createBoundingBoxAndGetEdges,
handleMovingBoundingBox,
SelectedEdge,
} from "oxalis/controller/combinations/bounding_box_handlers";
import {
Expand Down Expand Up @@ -561,12 +562,16 @@ export class BoundingBoxTool {
delta: Point2,
pos: Point2,
_id: string | null | undefined,
_event: MouseEvent,
event: MouseEvent,
) => {
if (primarySelectedEdge != null) {
handleResizingBoundingBox(pos, planeId, primarySelectedEdge, secondarySelectedEdge);
} else {
if (primarySelectedEdge == null) {
MoveHandlers.handleMovePlane(delta);
return;
}
if (event.ctrlKey || event.metaKey) {
handleMovingBoundingBox(delta, planeId, primarySelectedEdge);
} else {
handleResizingBoundingBox(pos, planeId, primarySelectedEdge, secondarySelectedEdge);
}
},
leftMouseDown: (pos: Point2, _plane: OrthoView, _event: MouseEvent) => {
Expand Down Expand Up @@ -596,7 +601,7 @@ export class BoundingBoxTool {
mouseMove: (delta: Point2, position: Point2, _id: any, event: MouseEvent) => {
if (primarySelectedEdge == null && planeId !== OrthoViews.TDView) {
MoveHandlers.moveWhenAltIsPressed(delta, position, _id, event);
highlightAndSetCursorOnHoveredBoundingBox(position, planeId);
highlightAndSetCursorOnHoveredBoundingBox(position, planeId, event);
}
},
rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => {
Expand All @@ -609,12 +614,12 @@ export class BoundingBoxTool {
_activeTool: AnnotationTool,
_useLegacyBindings: boolean,
_shiftKey: boolean,
_ctrlOrMetaKey: boolean,
ctrlOrMetaKey: boolean,
_altKey: boolean,
_isTDViewportActive: boolean,
): ActionDescriptor {
return {
leftDrag: "Create/Resize Bounding Boxes",
leftDrag: ctrlOrMetaKey ? "Move Bounding Boxes" : "Create/Resize Bounding Boxes",
rightClick: "Context Menu",
};
}
Expand Down
50 changes: 50 additions & 0 deletions frontend/javascripts/oxalis/model/accessors/view_mode_accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,40 @@ function _calculateMaybePlaneScreenPos(
return point;
}

function _calculateMaybeGlobalDelta(
state: OxalisState,
delta: Point2,
planeId?: OrthoView | null | undefined,
): Vector3 | null | undefined {
let position: Vector3;
planeId = planeId || state.viewModeData.plane.activeViewport;
const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale);
const diffX = delta.x * state.flycam.zoomStep;
const diffY = delta.y * state.flycam.zoomStep;

switch (planeId) {
case OrthoViews.PLANE_XY: {
position = [Math.round(diffX * planeRatio[0]), Math.round(diffY * planeRatio[1]), 0];
break;
}

case OrthoViews.PLANE_YZ: {
position = [0, Math.round(diffY * planeRatio[1]), Math.round(diffX * planeRatio[2])];
break;
}

case OrthoViews.PLANE_XZ: {
position = [Math.round(diffX * planeRatio[0]), 0, Math.round(diffY * planeRatio[2])];
break;
}

default:
return null;
}

return position;
}

function _calculateGlobalPos(
state: OxalisState,
clickPos: Point2,
Expand All @@ -206,6 +240,21 @@ function _calculateGlobalPos(
return position;
}

function _calculateGlobalDelta(
state: OxalisState,
delta: Point2,
planeId?: OrthoView | null | undefined,
): Vector3 {
const position = _calculateMaybeGlobalDelta(state, delta, planeId);

if (!position) {
console.error("Trying to calculate the global position, but no data viewport is active.");
return [0, 0, 0];
}

return position;
}

export function getDisplayedDataExtentInPlaneMode(state: OxalisState) {
const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale);
const curGlobalCenterPos = getPosition(state.flycam);
Expand Down Expand Up @@ -238,6 +287,7 @@ export function getDisplayedDataExtentInPlaneMode(state: OxalisState) {
}
export const calculateMaybeGlobalPos = reuseInstanceOnEquality(_calculateMaybeGlobalPos);
export const calculateGlobalPos = reuseInstanceOnEquality(_calculateGlobalPos);
export const calculateGlobalDelta = reuseInstanceOnEquality(_calculateGlobalDelta);
export const calculateMaybePlaneScreenPos = reuseInstanceOnEquality(_calculateMaybePlaneScreenPos);
export function getViewMode(state: OxalisState): ViewMode {
return state.temporaryConfiguration.viewMode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ class BoundingBox {
const size = this.getSize();
return { topLeft: this.min, width: size[0], height: size[1], depth: size[2] };
}

toBoundingBoxType(): BoundingBoxType {
return {
min: this.min,
max: this.max,
};
}
}

export default BoundingBox;
20 changes: 16 additions & 4 deletions frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import _ from "lodash";
import { getAdditionalCoordinatesAsString } from "../accessors/flycam_accessor";
import { getMeshesForAdditionalCoordinates } from "../accessors/volumetracing_accessor";
import { AdditionalCoordinate } from "types/api_flow_types";
import { getDatasetBoundingBox } from "../accessors/dataset_accessor";
import BoundingBox from "../bucket_data_handling/bounding_box";

const updateTracing = (state: OxalisState, shape: Partial<OxalisState["tracing"]>): OxalisState =>
updateKey(state, "tracing", shape);
Expand Down Expand Up @@ -178,17 +180,27 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState {
max: V3.toArray(V3.round(V3.add(action.center, halfBoxExtent))),
};
}
let newBoundingBox: UserBoundingBox;
let newUserBoundingBox: UserBoundingBox;
if (action.newBoundingBox != null) {
newBoundingBox = {
newUserBoundingBox = {
...newBoundingBoxTemplate,
...action.newBoundingBox,
};
} else {
newBoundingBox = newBoundingBoxTemplate;
newUserBoundingBox = newBoundingBoxTemplate;
}

const updatedUserBoundingBoxes = [...userBoundingBoxes, newBoundingBox];
// Ensure the new bounding box is within the dataset bounding box.
const datasetBoundingBox = getDatasetBoundingBox(state.dataset);
const newBoundingBox = new BoundingBox(newUserBoundingBox.boundingBox);
const newBoundingBoxWithinDataset = newBoundingBox.intersectedWith(datasetBoundingBox);
// Only update the bounding box if the bounding box overlaps with the dataset bounds.
// Else the bounding box is completely outside the dataset bounds -> in that case just keep the bounding box and let the user cook.
if (newBoundingBoxWithinDataset.getVolume() > 0) {
newUserBoundingBox.boundingBox = newBoundingBoxWithinDataset.toBoundingBoxType();
}

const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox];
return updateUserBoundingBoxes(state, updatedUserBoundingBoxes);
}

Expand Down
17 changes: 4 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,23 +220,14 @@
"**/rc-tree": "^5.7.12"
},
"ava": {
"files": [
"./public-test/test-bundle/**/*.{js,jsx}"
],
"ignoredByWatcher": [
"./binaryData/**/*.*"
],
"require": [
"./frontend/javascripts/test/_ava_polyfill_provider.ts"
],
"files": ["./public-test/test-bundle/**/*.{js,jsx}"],
"ignoredByWatcher": ["./binaryData/**/*.*"],
"require": ["./frontend/javascripts/test/_ava_polyfill_provider.ts"],
"snapshotDir": "frontend/javascripts/test/snapshots",
"concurrency": 8
},
"c8": {
"exclude": [
"public-test/test-bundle/test/**/*.*",
"frontend/javascripts/test/**/*.*"
],
"exclude": ["public-test/test-bundle/test/**/*.*", "frontend/javascripts/test/**/*.*"],
"reporter": "lcov"
}
}

0 comments on commit 93314fe

Please sign in to comment.