Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bounding Box tool improvements #7892

Merged
merged 16 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
philippotto marked this conversation as resolved.
Show resolved Hide resolved

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"
}
}