From 09506bd2b0ca08b92bb82e25954a227b2892b840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 1 Oct 2021 17:04:21 +0200 Subject: [PATCH 01/34] Add bounding box tool to tollbar (UI only) --- frontend/javascripts/oxalis/constants.js | 1 + .../controller/combinations/tool_controls.js | 75 +++++++++++++++++++ .../controller/viewmodes/plane_controller.js | 8 ++ .../oxalis/model/accessors/tool_accessor.js | 14 +++- .../oxalis/view/action-bar/toolbar_view.js | 9 +++ 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/oxalis/constants.js b/frontend/javascripts/oxalis/constants.js index 48cf1418ec0..af1630ba8f2 100644 --- a/frontend/javascripts/oxalis/constants.js +++ b/frontend/javascripts/oxalis/constants.js @@ -156,6 +156,7 @@ export const AnnotationToolEnum = { ERASE_TRACE: "ERASE_TRACE", FILL_CELL: "FILL_CELL", PICK_CELL: "PICK_CELL", + BOUNDING_BOX: "BOUNDING_BOX", }; export const VolumeTools = [ AnnotationToolEnum.BRUSH, diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 1a44b3d582f..ab49df371c2 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -17,8 +17,10 @@ import { handleAgglomerateSkeletonAtClick } from "oxalis/controller/combinations import { hideBrushAction } from "oxalis/model/actions/volumetracing_actions"; import { isBrushTool } from "oxalis/model/accessors/tool_accessor"; import * as MoveHandlers from "oxalis/controller/combinations/move_handlers"; +import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import PlaneView from "oxalis/view/plane_view"; import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers"; +import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; import Store from "oxalis/store"; import * as Utils from "libs/utils"; import * as VolumeHandlers from "oxalis/controller/combinations/volume_handlers"; @@ -521,9 +523,82 @@ export class FillCellTool { } } +export class BoundingBoxTool { + static getPlaneMouseControls( + _planeId: OrthoView, + planeView: PlaneView, + showNodeContextMenuAt: ShowContextMenuFunction, + ): * { + return { + leftDownMove: (delta: Point2, _pos: Point2, _id: ?string, _event: MouseEvent) => { + MoveHandlers.handleMovePlane(delta); + }, + leftMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + console.log("BoundingBox tool left down"); + }, + + leftMouseUp: () => { + console.log("BoundingBox tool left up"); + }, + + rightDownMove: (delta: Point2, pos: Point2) => { + console.log("BoundingBox tool right down move"); + }, + + rightMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + console.log("BoundingBox tool right down"); + }, + + rightMouseUp: () => { + console.log("BoundingBox tool right up"); + }, + + leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); + const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); + const boundingBoxId = highestBoundingBoxId + 1; + const newUserBoundingBox = { + boundingBox: { min: [100, 100, 100], max: [200, 200, 200] }, + id: boundingBoxId, + name: `user bounding box ${boundingBoxId}`, + color: Utils.getRandomColor(), + isVisible: true, + }; + const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; + Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); + }, + + rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { + console.log("BoundingBox right click"); + }, + }; + } + + static getActionDescriptors( + activeTool: AnnotationTool, + useLegacyBindings: boolean, + _shiftKey: boolean, + _ctrlKey: boolean, + _altKey: boolean, + ): Object { + let rightClick; + if (!useLegacyBindings) { + rightClick = "Context Menu"; + } else { + rightClick = `Erase (${activeTool === AnnotationToolEnum.BRUSH ? "Brush" : "Trace"})`; + } + + return { + leftDrag: activeTool === AnnotationToolEnum.BRUSH ? "Brush" : "Trace", + rightClick, + }; + } +} + const toolToToolClass = { [AnnotationToolEnum.MOVE]: MoveTool, [AnnotationToolEnum.SKELETON]: SkeletonTool, + [AnnotationToolEnum.BOUNDING_BOX]: BoundingBoxTool, [AnnotationToolEnum.BRUSH]: DrawTool, [AnnotationToolEnum.TRACE]: DrawTool, [AnnotationToolEnum.ERASE_TRACE]: EraseTool, diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js index 50e87f9fd4a..40ad2523f6e 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js @@ -41,6 +41,7 @@ import { EraseTool, PickCellTool, FillCellTool, + BoundingBoxTool, } from "oxalis/controller/combinations/tool_controls"; import constants, { type ShowContextMenuFunction, @@ -228,6 +229,11 @@ class PlaneController extends React.PureComponent { ); const fillCellControls = FillCellTool.getPlaneMouseControls(planeId); const pickCellControls = PickCellTool.getPlaneMouseControls(planeId); + const boundingBoxControls = BoundingBoxTool.getPlaneMouseControls( + planeId, + this.planeView, + this.props.showContextMenuAt, + ); const allControlKeys = _.union( Object.keys(moveControls), @@ -236,6 +242,7 @@ class PlaneController extends React.PureComponent { Object.keys(eraseControls), Object.keys(fillCellControls), Object.keys(pickCellControls), + Object.keys(boundingBoxControls), ); const controls = {}; @@ -249,6 +256,7 @@ class PlaneController extends React.PureComponent { [AnnotationToolEnum.ERASE_TRACE]: eraseControls[controlKey], [AnnotationToolEnum.PICK_CELL]: pickCellControls[controlKey], [AnnotationToolEnum.FILL_CELL]: fillCellControls[controlKey], + [AnnotationToolEnum.BOUNDING_BOX]: boundingBoxControls[controlKey], }); } diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.js b/frontend/javascripts/oxalis/model/accessors/tool_accessor.js index 83aaf1f74b2..8bc6f037bb3 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.js @@ -63,11 +63,12 @@ function _getDisabledInfoWhenVolumeIsDisabled( isDisabled: true, explanation: genericDisabledExplanation, }; + const notDisabledInfo = { + isDisabled: false, + explanation: "", + }; return { - [AnnotationToolEnum.MOVE]: { - isDisabled: false, - explanation: "", - }, + [AnnotationToolEnum.MOVE]: notDisabledInfo, [AnnotationToolEnum.SKELETON]: { isDisabled: !hasSkeleton, explanation: disabledSkeletonExplanation, @@ -78,6 +79,7 @@ function _getDisabledInfoWhenVolumeIsDisabled( [AnnotationToolEnum.ERASE_TRACE]: disabledInfo, [AnnotationToolEnum.FILL_CELL]: disabledInfo, [AnnotationToolEnum.PICK_CELL]: disabledInfo, + [AnnotationToolEnum.BOUNDING_BOX]: notDisabledInfo, }; } const getDisabledInfoWhenVolumeIsDisabled = memoizeOne(_getDisabledInfoWhenVolumeIsDisabled); @@ -122,6 +124,10 @@ function _getDisabledInfoFromArgs( isDisabled: false, explanation: genericDisabledExplanation, }, + [AnnotationToolEnum.BOUNDING_BOX]: { + isDisabled: false, + explanation: disabledSkeletonExplanation, + }, }; } const getDisabledInfoFromArgs = memoizeOne(_getDisabledInfoFromArgs); diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js index 6389a614ba3..5354f225f6c 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js @@ -561,6 +561,15 @@ export default function ToolbarView() { }} /> + + BBox + ) : null} From 668063727ff4a3b4d1e1fdd5ae8b3fd6e38b7e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Tue, 5 Oct 2021 13:31:13 +0200 Subject: [PATCH 02/34] WIP bbox tool --- .../controller/combinations/tool_controls.js | 6 + .../oxalis/controller/scene_controller.js | 8 +- .../javascripts/oxalis/view/plane_view.js | 134 ++++++++++++++---- 3 files changed, 119 insertions(+), 29 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index ab49df371c2..16c9ebd2737 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -531,9 +531,11 @@ export class BoundingBoxTool { ): * { return { leftDownMove: (delta: Point2, _pos: Point2, _id: ?string, _event: MouseEvent) => { + planeView.throttledPerformBoundingBoxHitTest([_pos.x, _pos.y]); MoveHandlers.handleMovePlane(delta); }, leftMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + planeView.performBoundingBoxHitTest([pos.x, pos.y]); console.log("BoundingBox tool left down"); }, @@ -552,6 +554,10 @@ export class BoundingBoxTool { rightMouseUp: () => { console.log("BoundingBox tool right up"); }, + mouseMove: (delta: Point2, position: Point2, id, event) => { + // planeView.performIsosurfaceHitTest([position.x, position.y]); + planeView.throttledPerformBoundingBoxHitTest([position.x, position.y]); + }, leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 98f2624c61f..7daf80e096a 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -449,7 +449,7 @@ class SceneController { setUserBoundingBoxes(bboxes: Array): void { const newUserBoundingBoxGroup = new THREE.Group(); - this.userBoundingBoxes = bboxes.map(({ boundingBox, isVisible, color }) => { + this.userBoundingBoxes = bboxes.map(({ boundingBox, isVisible, color, id }) => { const { min, max } = boundingBox; const bbColor = [color[0] * 255, color[1] * 255, color[2] * 255]; const bbCube = new Cube({ @@ -459,9 +459,13 @@ class SceneController { showCrossSections: true, }); bbCube.setVisibility(isVisible); - bbCube.getMeshes().forEach(mesh => newUserBoundingBoxGroup.add(mesh)); + bbCube.getMeshes().forEach(mesh => { + newUserBoundingBoxGroup.add(mesh); + mesh.userData.id = id; + }); return bbCube; }); + console.log("resetting bboxes"); this.rootNode.remove(this.userBoundingBoxGroup); this.userBoundingBoxGroup = newUserBoundingBoxGroup; this.rootNode.add(this.userBoundingBoxGroup); diff --git a/frontend/javascripts/oxalis/view/plane_view.js b/frontend/javascripts/oxalis/view/plane_view.js index 2246bc1f592..9824633f6a8 100644 --- a/frontend/javascripts/oxalis/view/plane_view.js +++ b/frontend/javascripts/oxalis/view/plane_view.js @@ -12,8 +12,9 @@ import Constants, { type OrthoViewMap, OrthoViewValues, OrthoViews, + type OrthoView, } from "oxalis/constants"; -import Store from "oxalis/store"; +import Store, { type OxalisState } from "oxalis/store"; import app from "app"; import getSceneController from "oxalis/controller/scene_controller_provider"; import window from "libs/window"; @@ -30,7 +31,7 @@ const createDirLight = (position, target, intensity, parent) => { }; const raycaster = new THREE.Raycaster(); -let oldRaycasterHit = null; +raycaster.params.Line.threshold = 100; const ISOSURFACE_HOVER_THROTTLING_DELAY = 150; @@ -41,8 +42,11 @@ class PlaneView { cameras: OrthoViewMap; throttledPerformIsosurfaceHitTest: ([number, number]) => ?typeof THREE.Vector3; + throttledPerformBoundingBoxHitTest: ([number, number]) => ?typeof THREE.Vector3; running: boolean; + lastIsosurfaceHit: ?typeof THREE.Object3D; + lastBoundingBoxHit: ?typeof THREE.Object3D; needsRerender: boolean; constructor() { @@ -51,7 +55,9 @@ class PlaneView { this.performIsosurfaceHitTest, ISOSURFACE_HOVER_THROTTLING_DELAY, ); - + this.throttledPerformBoundingBoxHitTest = _.throttle(this.performBoundingBoxHitTest, 75); + this.lastIsosurfaceHit = null; + this.lastBoundingBoxHit = null; this.running = false; const { scene } = getSceneController(); @@ -146,15 +152,37 @@ class PlaneView { } } + performHitTestForSceneGroup( + storeState: OxalisState, + groupToTest: typeof THREE.Group, + mousePosition: [number, number], + orthoView: OrthoView, + ): ?typeof THREE.Intersection { + const viewport = getInputCatcherRect(storeState, orthoView); + // Perform ray casting + const mouse = new THREE.Vector2( + (mousePosition[0] / viewport.width) * 2 - 1, + ((mousePosition[1] / viewport.height) * 2 - 1) * -1, // y is inverted + ); + + raycaster.setFromCamera(mouse, this.cameras[orthoView]); + // The second parameter of intersectObjects is set to true to ensure that + // the groups which contain the actual meshes are traversed. + const intersections = raycaster.intersectObjects(groupToTest.children, true); + const intersection = intersections.length > 0 ? intersections[0] : null; + /* intersections.forEach(({ object: hitObject }) => { + hitObject.material.color.r = 1; + hitObject.material.color.g = 0; + hitObject.material.color.b = 0; + }); */ + return intersection; + } + performIsosurfaceHitTest(mousePosition: [number, number]): ?typeof THREE.Vector3 { const storeState = Store.getState(); - const SceneController = getSceneController(); - const { isosurfacesRootGroup } = SceneController; - const tdViewport = getInputCatcherRect(storeState, "TDView"); - const { hoveredIsosurfaceId } = storeState.temporaryConfiguration; - // Outside of the 3D viewport, we don't do isosurface hit tests if (storeState.viewModeData.plane.activeViewport !== OrthoViews.TDView) { + const { hoveredIsosurfaceId } = storeState.temporaryConfiguration; if (hoveredIsosurfaceId !== 0) { // Reset hoveredIsosurfaceId if we are outside of the 3D viewport, // since that id takes precedence over the shader-calculated cell id @@ -164,49 +192,101 @@ class PlaneView { return null; } - // Perform ray casting - const mouse = new THREE.Vector2( - (mousePosition[0] / tdViewport.width) * 2 - 1, - ((mousePosition[1] / tdViewport.height) * 2 - 1) * -1, // y is inverted + const SceneController = getSceneController(); + const { isosurfacesRootGroup } = SceneController; + const intersection = this.performHitTestForSceneGroup( + storeState, + isosurfacesRootGroup, + mousePosition, + "TDView", ); - - raycaster.setFromCamera(mouse, this.cameras[OrthoViews.TDView]); - // The second parameter of intersectObjects is set to true to ensure that - // the groups which contain the actual meshes are traversed. - const intersections = raycaster.intersectObjects(isosurfacesRootGroup.children, true); - const hitObject = intersections.length > 0 ? intersections[0].object : null; - + const hitObject = intersection != null ? intersection.object : null; // Check whether we are hitting the same object as before, since we can return early // in this case. - if (hitObject === oldRaycasterHit) { - return intersections.length > 0 ? intersections[0].point : null; + if (hitObject === this.lastIsosurfaceHit) { + return intersection != null ? intersection.point : null; } // Undo highlighting of old hit - if (oldRaycasterHit != null) { - oldRaycasterHit.parent.children.forEach(meshPart => { + if (this.lastIsosurfaceHit != null) { + this.lastIsosurfaceHit.parent.children.forEach(meshPart => { meshPart.material.emissive.setHex("#000000"); }); - oldRaycasterHit = null; } - oldRaycasterHit = hitObject; + this.lastIsosurfaceHit = hitObject; // Highlight new hit - if (hitObject != null) { + if (hitObject != null && intersection != null) { const hoveredColor = [0.7, 0.5, 0.1]; hitObject.parent.children.forEach(meshPart => { meshPart.material.emissive.setHSL(...hoveredColor); }); Store.dispatch(updateTemporarySettingAction("hoveredIsosurfaceId", hitObject.parent.cellId)); - return intersections[0].point; + return intersection.point; } else { Store.dispatch(updateTemporarySettingAction("hoveredIsosurfaceId", 0)); return null; } } + performBoundingBoxHitTest(mousePosition: [number, number]): ?typeof THREE.Vector3 { + const storeState = Store.getState(); + const { activeViewport } = storeState.viewModeData.plane; + // Currently, the bounding box tool only supports the 2d viewports. + if (activeViewport === OrthoViews.TDView) { + return null; + } + + const SceneController = getSceneController(); + const { userBoundingBoxGroup } = SceneController; + const intersection = this.performHitTestForSceneGroup( + storeState, + userBoundingBoxGroup, + mousePosition, + activeViewport, + ); + console.log(intersection); + const hitObject = intersection != null ? intersection.object : null; + // Check whether we are hitting the same object as before, since we can return early + // in this case. + if (hitObject === this.lastBoundingBoxHit) { + if (hitObject != null) { + console.log("Hit the same object"); + } + return intersection != null ? intersection.point : null; + } + + // Undo highlighting of old hit + if (this.lastBoundingBoxHit != null) { + // Get HSL, save in userData, light the hsl up and set the new color. + // changing emissive doesnt work for this material. + const { originalColor } = this.lastBoundingBoxHit.userData; + this.lastBoundingBoxHit.material.color.setHSL(originalColor); + } + + this.lastBoundingBoxHit = hitObject; + + // Highlight new hit + if (hitObject != null) { + // debugger; + // TODO: gucken warum sich die Farbe der BBoxen nicht ändert. Scheint das falsche intersection object zu sein!!!! + const hslColor = { h: 0, s: 0, l: 0 }; + // const hoveredColor = [0.7, 0.5, 0.1]; + hitObject.material.color.getHSL(hslColor); + hitObject.userData.originalColor = hslColor; + // const lightenedColor = { h: hslColor.h, s: 1, l: 1 }; + // hitObject.material.color.setHSL(lightenedColor); + // hitObject.material.color.setHSL(...hoveredColor); + hitObject.material.color.r = 1; + hitObject.material.color.g = 0; + hitObject.material.color.b = 0; + // (...hoveredColor); + } + return null; + } + draw(): void { app.vent.trigger("rerender"); } From 35ee289f0787b3587994302773829677f0b9b419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 7 Oct 2021 17:37:37 +0200 Subject: [PATCH 03/34] add hit planes to check for bbox crosssection hovering --- .../controller/combinations/tool_controls.js | 4 +- .../oxalis/controller/scene_controller.js | 12 +- .../javascripts/oxalis/geometries/cube.js | 200 +++++++++++++++++- .../javascripts/oxalis/view/plane_view.js | 24 ++- 4 files changed, 228 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 16c9ebd2737..4569813e4e3 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -560,7 +560,7 @@ export class BoundingBoxTool { }, leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { - const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); + /* const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); const boundingBoxId = highestBoundingBoxId + 1; const newUserBoundingBox = { @@ -571,7 +571,7 @@ export class BoundingBoxTool { isVisible: true, }; const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; - Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); + Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); */ }, rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 7daf80e096a..81c4543668b 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -53,6 +53,7 @@ class SceneController { planeShift: Vector3; datasetBoundingBox: Cube; userBoundingBoxGroup: typeof THREE.Group; + userBoundingBoxHitPlanesGroup: typeof THREE.Group; userBoundingBoxes: Array; taskBoundingBox: ?Cube; contour: ContourGeometry; @@ -275,6 +276,7 @@ class SceneController { createMeshes(): void { this.rootNode = new THREE.Object3D(); this.userBoundingBoxGroup = new THREE.Group(); + this.userBoundingBoxHitPlanesGroup = new THREE.Group(); this.rootNode.add(this.userBoundingBoxGroup); this.userBoundingBoxes = []; @@ -449,6 +451,7 @@ class SceneController { setUserBoundingBoxes(bboxes: Array): void { const newUserBoundingBoxGroup = new THREE.Group(); + const newUserBoundingBoxHitPlanesGroup = new THREE.Group(); this.userBoundingBoxes = bboxes.map(({ boundingBox, isVisible, color, id }) => { const { min, max } = boundingBox; const bbColor = [color[0] * 255, color[1] * 255, color[2] * 255]; @@ -457,18 +460,25 @@ class SceneController { max, color: Utils.rgbToInt(bbColor), showCrossSections: true, + id, + isEditable: true, }); bbCube.setVisibility(isVisible); bbCube.getMeshes().forEach(mesh => { newUserBoundingBoxGroup.add(mesh); mesh.userData.id = id; }); + bbCube + .getCrossSectionHitPlanes() + .forEach(hitPlane => newUserBoundingBoxHitPlanesGroup.add(hitPlane)); return bbCube; }); - console.log("resetting bboxes"); this.rootNode.remove(this.userBoundingBoxGroup); + this.rootNode.remove(this.userBoundingBoxHitPlanesGroup); this.userBoundingBoxGroup = newUserBoundingBoxGroup; + this.userBoundingBoxHitPlanesGroup = newUserBoundingBoxHitPlanesGroup; this.rootNode.add(this.userBoundingBoxGroup); + this.rootNode.add(this.userBoundingBoxHitPlanesGroup); } setSkeletonGroupVisibility(isVisible: boolean) { diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index eb4bb35df11..9d3aeed2d30 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -15,6 +15,7 @@ import { type Vector3, } from "oxalis/constants"; import { getPosition } from "oxalis/model/accessors/flycam_accessor"; +import ErrorHandling from "libs/error_handling"; import Store from "oxalis/throttled_store"; import app from "app"; import dimensions from "oxalis/model/dimensions"; @@ -25,21 +26,33 @@ type Properties = { lineWidth?: number, color?: number, showCrossSections?: boolean, + id?: number, + isEditable?: boolean, }; +type PlaneGeometry = typeof THREE.PlaneGeometry; +// type CrossSectionHitPlanesTuple = [PlaneGeometry, PlaneGeometry, PlaneGeometry, PlaneGeometry]; +type CrossSectionHitPlanesTuple = Array; + class Cube { crossSections: OrthoViewMap; + crossSectionHitPlanes: OrthoViewMap; + cube: typeof THREE.Line; min: Vector3; max: Vector3; showCrossSections: boolean; initialized: boolean; visible: boolean; + id: ?number; + isEditable: boolean; constructor(properties: Properties) { // min/max should denote a half-open interval. this.min = properties.min || [0, 0, 0]; this.max = properties.max; + this.id = properties.id; + this.isEditable = properties.isEditable || false; const lineWidth = properties.lineWidth != null ? properties.lineWidth : 1; const color = properties.color || 0x000000; this.showCrossSections = properties.showCrossSections || false; @@ -52,6 +65,11 @@ class Cube { this.cube = new THREE.Line(new THREE.Geometry(), new THREE.LineBasicMaterial(lineProperties)); this.crossSections = {}; + this.crossSectionHitPlanes = { + [OrthoViews.PLANE_XY]: [], + [OrthoViews.PLANE_YZ]: [], + [OrthoViews.PLANE_XZ]: [], + }; for (const planeId of OrthoViewValuesWithoutTDView) { this.crossSections[planeId] = new THREE.Line( new THREE.Geometry(), @@ -69,6 +87,134 @@ class Cube { ); } + getEdgeHitBox( + width: number, + height: number, + depth: number, + x: number, + y: number, + z: number, + direction: string, + edgeId: number, + ): typeof THREE.BoxGeometry { + const maxWidth = 40; + let geometry; + switch (direction) { + case "width": { + geometry = new THREE.BoxGeometry( + width + maxWidth, + Math.min(maxWidth, height / 3), + Math.min(maxWidth, depth / 3), + ); + break; + } + case "depth": { + geometry = new THREE.BoxGeometry( + Math.min(maxWidth, width / 3), + Math.min(maxWidth, height / 3), + depth + maxWidth, + ); + break; + } + case "height": { + geometry = new THREE.BoxGeometry( + Math.min(maxWidth, width / 3), + height + maxWidth, + Math.min(maxWidth, depth / 3), + ); + break; + } + default: { + geometry = new THREE.BoxGeometry(1, 1, 1); + } + } + const box = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: 0x00ff00 })); + box.position.set(x, y, z); + box.userData.boxId = this.id; + box.userData.edgeId = edgeId; + return box; + } + + getHitPlane( + crossSectionWidth: number, + crossSectionHeight: number, + topLeftOfEdge: [number, number, number], + direction: "width" | "height", + extendDirectionIndex: 0 | 1 | 2, + edgeId: number, + ): typeof THREE.PlaneGeometry { + const maxHitOffset = 40; + let planeWidth; + let planeHeight; + if (direction === "width") { + planeWidth = crossSectionWidth + maxHitOffset; + planeHeight = Math.min(maxHitOffset, crossSectionHeight / 3); + topLeftOfEdge[extendDirectionIndex] += crossSectionWidth / 2; + } else { + planeWidth = Math.min(maxHitOffset, crossSectionWidth / 3); + planeHeight = crossSectionHeight + maxHitOffset; + topLeftOfEdge[extendDirectionIndex] += crossSectionHeight / 2; + } + const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight); + // TODO: Adjust Orientation of the planes according to the plane parameter!!!! + const plane = new THREE.Mesh( + geometry, + new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide, opacity: 0.3 }), + ); + const [x, y, z] = topLeftOfEdge; + plane.position.set(x, y, z); + plane.userData.boxId = this.id; + plane.userData.edgeId = edgeId; + return plane; + } + + getXYPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { + const { min } = this; + const { max } = this; + const width = max[0] - min[0]; + const height = max[1] - min[1]; + return [ + this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 1, 0), + this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 1), + this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 1, 2), + this.getHitPlane(width, height, [min[0], max[1], min[2]], "width", 0, 3), + ]; + } + + getYZPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { + const { min } = this; + const { max } = this; + const width = max[1] - min[1]; + const height = max[2] - min[2]; + const planes = [ + this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 2, 4), + this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 1, 5), + this.getHitPlane(width, height, [min[0], max[1], min[2]], "height", 2, 6), + this.getHitPlane(width, height, [min[0], min[1], max[2]], "width", 1, 7), + ]; + planes.forEach(plane => { + plane.geometry.rotateY(Math.PI / 2); + }); + return planes; + } + + getXZPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { + const { min } = this; + const { max } = this; + const width = max[0] - min[0]; + const height = max[2] - min[2]; + const planes = [ + this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 2, 8), + this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 9), + this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 2, 10), + this.getHitPlane(width, height, [min[0], min[1], max[2]], "width", 0, 11), + ]; + planes.forEach(plane => { + plane.geometry.rotateX(Math.PI / 2); + }); + return planes; + } + setCorners(min: Vector3, max: Vector3) { this.min = min; this.max = max; @@ -122,6 +268,11 @@ class Cube { vec(min[0], 0, min[2]), ]; + if (this.isEditable) { + ErrorHandling.assert(this.id != null, "Every editable bounding box needs an id!"); + this.createEdgeHitboxesForCrossSections(); + } + for (const mesh of _.values(this.crossSections).concat([this.cube])) { mesh.geometry.computeBoundingSphere(); mesh.geometry.verticesNeedUpdate = true; @@ -132,14 +283,42 @@ class Cube { app.vent.trigger("rerender"); } + createEdgeHitboxesForCrossSections() { + // The bounding boxes for the edges of the cube have an id equal to the index of the array. + // The index of the edge is illustrated in the following ascii art. + // min--> +-----1------+ + // .'| .'| + // 0' 4 2' | + // .' | .' 5 + // +----3-------+ | + // | | | | + // | +---9-|------+ + // 7 .' 6 .' + // | .8 | 10 + // |.' | .' + // +----11------+' <-- max + // + // -z + // ▵ ↗ -y + // | .' + // | .' + // |.' + // ----------> +x + // this.crossSectionHitPlanes[OrthoViews.PLANE_XY] = this.getXYPlaneCrossSectionHitPlanes(); + this.crossSectionHitPlanes[OrthoViews.PLANE_XY] = []; + this.crossSectionHitPlanes[OrthoViews.PLANE_YZ] = []; + this.crossSectionHitPlanes[OrthoViews.PLANE_XZ] = this.getXZPlaneCrossSectionHitPlanes(); + // this.crossSectionHitPlanes[OrthoViews.PLANE_XZ] = []; + } + updatePosition(position: Vector3) { if (!this.initialized) { return; } - + // Why not generally avoid updating when the position for the third dim is outside the bbox? for (const planeId of OrthoViewValuesWithoutTDView) { const thirdDim = dimensions.thirdDimensionForPlane(planeId); - const geometry = this.crossSections[planeId].geometry; + const { geometry } = this.crossSections[planeId]; for (const vertex of geometry.vertices) { const array = vertex.toArray(); array[thirdDim] = position[thirdDim]; @@ -148,6 +327,19 @@ class Cube { geometry.computeBoundingSphere(); geometry.verticesNeedUpdate = true; + if (position[thirdDim] >= this.min[thirdDim] && position[thirdDim] < this.max[thirdDim]) { + for (const hitPlane of this.crossSectionHitPlanes[planeId]) { + const hitPlanePosition = hitPlane.position.toArray(); + hitPlanePosition[thirdDim] = position[thirdDim]; + hitPlane.position.set(hitPlanePosition[0], hitPlanePosition[1], hitPlanePosition[2]); + hitPlane.geometry.verticesNeedUpdate = true; + console.log(hitPlane); + // hitPlane.geometry.attributes.position.needsUpdate = true; + // TODO: "The bounding sphere is also computed automatically when doing raycasting." + // According to this, the call can be removed and will then only be done when needed. + hitPlane.geometry.computeBoundingBox(); + } + } } } @@ -155,6 +347,10 @@ class Cube { return [this.cube].concat(_.values(this.crossSections)); } + getCrossSectionHitPlanes(): Array { + return _.flattenDeep(_.values(this.crossSectionHitPlanes)); + } + updateForCam(id: OrthoView) { if (!this.initialized) { return; diff --git a/frontend/javascripts/oxalis/view/plane_view.js b/frontend/javascripts/oxalis/view/plane_view.js index 9824633f6a8..e2f0d3dcb6d 100644 --- a/frontend/javascripts/oxalis/view/plane_view.js +++ b/frontend/javascripts/oxalis/view/plane_view.js @@ -157,6 +157,7 @@ class PlaneView { groupToTest: typeof THREE.Group, mousePosition: [number, number], orthoView: OrthoView, + clipAtDistanceZero: boolean, ): ?typeof THREE.Intersection { const viewport = getInputCatcherRect(storeState, orthoView); // Perform ray casting @@ -164,8 +165,14 @@ class PlaneView { (mousePosition[0] / viewport.width) * 2 - 1, ((mousePosition[1] / viewport.height) * 2 - 1) * -1, // y is inverted ); - - raycaster.setFromCamera(mouse, this.cameras[orthoView]); + let camera = this.cameras[orthoView]; + if (clipAtDistanceZero) { + camera = camera.clone(false); + // Only pick what is being rendered in the ortho viewports. + camera.far = 0.1; + camera.updateProjectionMatrix(); + } + raycaster.setFromCamera(mouse, camera); // The second parameter of intersectObjects is set to true to ensure that // the groups which contain the actual meshes are traversed. const intersections = raycaster.intersectObjects(groupToTest.children, true); @@ -199,6 +206,7 @@ class PlaneView { isosurfacesRootGroup, mousePosition, "TDView", + false, ); const hitObject = intersection != null ? intersection.object : null; // Check whether we are hitting the same object as before, since we can return early @@ -240,12 +248,13 @@ class PlaneView { } const SceneController = getSceneController(); - const { userBoundingBoxGroup } = SceneController; + const { userBoundingBoxHitPlanesGroup } = SceneController; const intersection = this.performHitTestForSceneGroup( storeState, - userBoundingBoxGroup, + userBoundingBoxHitPlanesGroup, mousePosition, activeViewport, + true, ); console.log(intersection); const hitObject = intersection != null ? intersection.object : null; @@ -257,13 +266,14 @@ class PlaneView { } return intersection != null ? intersection.point : null; } - + console.log(hitObject); // Undo highlighting of old hit if (this.lastBoundingBoxHit != null) { // Get HSL, save in userData, light the hsl up and set the new color. // changing emissive doesnt work for this material. - const { originalColor } = this.lastBoundingBoxHit.userData; - this.lastBoundingBoxHit.material.color.setHSL(originalColor); + this.lastBoundingBoxHit.material.color.r = 0; + this.lastBoundingBoxHit.material.color.g = 1; + this.lastBoundingBoxHit.material.color.b = 0; } this.lastBoundingBoxHit = hitObject; From b87ebe7c71001e61fba3f691f68e31381f8e8261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 8 Oct 2021 17:10:12 +0200 Subject: [PATCH 04/34] add manual bbox hovering check --- .../combinations/bounding_box_handlers.js | 146 ++++++++++++++++++ .../controller/combinations/tool_controls.js | 73 ++++++++- .../javascripts/oxalis/geometries/cube.js | 92 ++++++----- .../javascripts/oxalis/view/plane_view.js | 55 ++++--- 4 files changed, 292 insertions(+), 74 deletions(-) create mode 100644 frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js new file mode 100644 index 00000000000..2cfe6e5c959 --- /dev/null +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -0,0 +1,146 @@ +// @flow +import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor"; +import { type OrthoView, type Point2, type Vector3 } from "oxalis/constants"; +import Store from "oxalis/store"; +import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; +import Dimension from "oxalis/model/dimensions"; + +const neighbourEdgeIndexByEdgeIndex = { + // The edges are indexed within the plane like this: + // See the distanceArray calculation as a reference. + // +---0---+ + // | | + // 2 3 + // | | + // +---1---+ + // + "0": [2, 3], + "1": [2, 3], + "2": [0, 1], + "3": [0, 1], +}; +const MAX_DISTANCE_TO_SELECTION = 40; + +function getDistanceToBoundingBoxEdge( + pos: Vector3, + min: Vector3, + max: Vector3, + compareToMin: boolean, + edgeDim: number, + otherDim: number, +) { + // There are four cases how the distance to an edge needs to be calculated. + // Here are all cases visualized via a number that are referenced below: + // Note that this is the perspective of the rendered bounding box cross section. + // ---> x + // | 1 1 + // ↓ '. .' + // y ↘ ↙ + // +-------+ + // | | + // 3 --> | | <-- 3 + // | | + // +-------+ + // ↗ ↖ + // .' '. + // 2 2 + // + // This example is for the xy viewport for x as the main direction / edgeDim. + const cornerToCompareWith = compareToMin ? min : max; + if (pos[otherDim] < min[otherDim]) { + // Case 1: Distance to the min corner is needed in otherDim. + return Math.sqrt( + Math.abs(pos[edgeDim] - cornerToCompareWith[edgeDim]) ** 2 + + Math.abs(pos[otherDim] - min[otherDim]) ** 2, + ); + } + if (pos[otherDim] > max[otherDim]) { + // Case 2: Distance to max Corner is needed in otherDim. + return Math.sqrt( + Math.abs(pos[edgeDim] - cornerToCompareWith[edgeDim]) ** 2 + + Math.abs(pos[otherDim] - max[otherDim]) ** 2, + ); + } + // Case 3: + // If the position is within the bounds of the other dimension, the shortest distance + // to the edge is simply the difference between the edgeDim values. + return Math.abs(pos[edgeDim] - cornerToCompareWith[edgeDim]); +} + +export default function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView) { + const state = Store.getState(); + const globalPosition = calculateGlobalPos(state, pos); + const { userBoundingBoxes } = getSomeTracing(state.tracing); + const reorderedIndices = Dimension.getIndices(plane); + const thirdDim = reorderedIndices[2]; + + let currentNearestDistance = MAX_DISTANCE_TO_SELECTION; + let currentNearestBoundingBox = null; + let currentNearestDistanceArray = null; + + for (const bbox of userBoundingBoxes) { + const { min, max } = bbox.boundingBox; + const isCrossSectionOfViewportVisible = + globalPosition[thirdDim] >= min[thirdDim] && globalPosition[thirdDim] < max[thirdDim]; + if (!isCrossSectionOfViewportVisible) { + break; + } + const distanceArray = [ + getDistanceToBoundingBoxEdge( + globalPosition, + min, + max, + true, + reorderedIndices[0], + reorderedIndices[1], + ), + getDistanceToBoundingBoxEdge( + globalPosition, + min, + max, + false, + reorderedIndices[0], + reorderedIndices[1], + ), + getDistanceToBoundingBoxEdge( + globalPosition, + min, + max, + true, + reorderedIndices[1], + reorderedIndices[0], + ), + getDistanceToBoundingBoxEdge( + globalPosition, + min, + max, + false, + reorderedIndices[1], + reorderedIndices[0], + ), + ]; + const minimumDistance = Math.min(...distanceArray); + if (minimumDistance < currentNearestDistance) { + currentNearestDistance = minimumDistance; + currentNearestBoundingBox = bbox; + currentNearestDistanceArray = distanceArray; + } + } + if (currentNearestBoundingBox == null || currentNearestDistanceArray == null) { + return null; + } + // TODO: nearestEdgeIndex seems wrong. should be 0 or 1 instead of 2 or 3 and other way round. + // maybe a little logic error in getDistanceToBoundingBoxEdge or when calling this method. + const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance); + const dimensionOfNearestEdge = nearestEdgeIndex < 2 ? reorderedIndices[0] : reorderedIndices[1]; + const edgeDirection = nearestEdgeIndex < 2 ? "vertical" : "horizontal"; + const isMaxEdge = nearestEdgeIndex % 2 === 1; + // TODO: Add feature to select corners. + return { + boxId: currentNearestBoundingBox.id, + dimensionIndex: dimensionOfNearestEdge, + edgeDirection, + isMaxEdge, + nearestEdgeIndex, + }; +} diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 4569813e4e3..7e83caa2817 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -1,5 +1,6 @@ // @flow import { type ModifierKeys } from "libs/input"; +import _ from "lodash"; import { type OrthoView, OrthoViews, @@ -17,11 +18,14 @@ import { handleAgglomerateSkeletonAtClick } from "oxalis/controller/combinations import { hideBrushAction } from "oxalis/model/actions/volumetracing_actions"; import { isBrushTool } from "oxalis/model/accessors/tool_accessor"; import * as MoveHandlers from "oxalis/controller/combinations/move_handlers"; +import { edgeIdToEdge } from "oxalis/geometries/cube"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import PlaneView from "oxalis/view/plane_view"; import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers"; +import getClosestHoveredBoundingBox from "oxalis/controller/combinations/bounding_box_handlers"; import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; import Store from "oxalis/store"; +import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import * as Utils from "libs/utils"; import * as VolumeHandlers from "oxalis/controller/combinations/volume_handlers"; import api from "oxalis/api/internal_api"; @@ -525,22 +529,60 @@ export class FillCellTool { export class BoundingBoxTool { static getPlaneMouseControls( - _planeId: OrthoView, + planeId: OrthoView, planeView: PlaneView, showNodeContextMenuAt: ShowContextMenuFunction, ): * { + let selectedEdge = null; return { leftDownMove: (delta: Point2, _pos: Point2, _id: ?string, _event: MouseEvent) => { - planeView.throttledPerformBoundingBoxHitTest([_pos.x, _pos.y]); - MoveHandlers.handleMovePlane(delta); + if (selectedEdge != null) { + const state = Store.getState(); + const currentlySelectedEdge = selectedEdge; // avoiding flow complains. + const { userBoundingBoxes } = getSomeTracing(state.tracing); + const zoomFactor = state.flycam.zoomStep; + // TODO: Discussion aufschreiben: + // Raycaster nutzt gpu nicht -> nicht all zu fix wie gedacht. + // Raycaster benötigt zusätzlich noch weitere objekte, die immer mitgeupdatet werden müssen (spätestens wenn das bbox tool an ist) + // Manuelles Testen wäre nicht schlauer gemacht / schneller + + // Generelle Frage: Was wenn man mehrere BBox kandidaten hat, beim hoveren? eine eigene berechnung wäre da fixer: + // Pro Bounding Box checken nur ein check nötig, da die plane explizit bekannt ist, + const scaleFactor = getBaseVoxelFactors(state.dataset.dataSource.scale); + const updatedUserBoundingBoxes = userBoundingBoxes.map(bbox => { + if (bbox.id !== currentlySelectedEdge.boxId) { + return bbox; + } + bbox = _.cloneDeep(bbox); + // For a horizontal edge only consider delta.y, for vertical only delta.x + const movement = currentlySelectedEdge.direction === "horizontal" ? delta.y : delta.x; + const minOrMax = currentlySelectedEdge.isMaxEdge ? "max" : "min"; + const scaledMovement = + movement * zoomFactor * scaleFactor[currentlySelectedEdge.dimensionIndex]; + bbox.boundingBox[minOrMax][currentlySelectedEdge.dimensionIndex] = Math.round( + bbox.boundingBox[minOrMax][currentlySelectedEdge.dimensionIndex] + scaledMovement, + ); + return bbox; + }); + + Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); + } else { + MoveHandlers.handleMovePlane(delta); + } }, leftMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { - planeView.performBoundingBoxHitTest([pos.x, pos.y]); - console.log("BoundingBox tool left down"); + const hitObject = planeView.performBoundingBoxHitTest([pos.x, pos.y]); + if (hitObject != null) { + const { userData } = hitObject; + selectedEdge = { + boxId: userData.boxId, + ...edgeIdToEdge(userData.edgeId, userData.plane), + }; + } }, leftMouseUp: () => { - console.log("BoundingBox tool left up"); + selectedEdge = null; }, rightDownMove: (delta: Point2, pos: Point2) => { @@ -555,8 +597,25 @@ export class BoundingBoxTool { console.log("BoundingBox tool right up"); }, mouseMove: (delta: Point2, position: Point2, id, event) => { + const { body } = document; + if (body == null || selectedEdge != null) { + return; + } + const info = getClosestHoveredBoundingBox(position, planeId); + console.log("info", info); // planeView.performIsosurfaceHitTest([position.x, position.y]); - planeView.throttledPerformBoundingBoxHitTest([position.x, position.y]); + /* const hitObject = planeView.throttledPerformBoundingBoxHitTest([position.x, position.y]); + if (hitObject != null) { + const { userData } = hitObject; + const { direction } = edgeIdToEdge(userData.edgeId, userData.plane); + if (direction === "horizontal") { + body.style.cursor = "row-resize"; + } else { + body.style.cursor = "col-resize"; + } + } else { + body.style.cursor = "default"; + } */ }, leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index 9d3aeed2d30..b31884cc80e 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -34,6 +34,28 @@ type PlaneGeometry = typeof THREE.PlaneGeometry; // type CrossSectionHitPlanesTuple = [PlaneGeometry, PlaneGeometry, PlaneGeometry, PlaneGeometry]; type CrossSectionHitPlanesTuple = Array; +export function edgeIdToEdge(id: number, plane: OrthoView) { + const isMaxEdge = id > 1; + const direction = id % 2 === 0 ? "vertical" : "horizontal"; + switch (plane) { + case OrthoViews.PLANE_XY: { + const dimensionIndex = id % 2 === 0 ? 0 : 1; + return { dimensionIndex, direction, isMaxEdge }; + } + case OrthoViews.PLANE_YZ: { + const dimensionIndex = id % 2 === 0 ? 2 : 1; + return { dimensionIndex, direction, isMaxEdge }; + } + case OrthoViews.PLANE_XZ: { + const dimensionIndex = id % 2 === 0 ? 0 : 2; + return { dimensionIndex, direction, isMaxEdge }; + } + default: { + return { dimensionIndex: 0, direction, isMaxEdge }; + } + } +} + class Cube { crossSections: OrthoViewMap; crossSectionHitPlanes: OrthoViewMap; @@ -159,12 +181,18 @@ class Cube { // TODO: Adjust Orientation of the planes according to the plane parameter!!!! const plane = new THREE.Mesh( geometry, - new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide, opacity: 0.3 }), + new THREE.MeshBasicMaterial({ + color: 0x00ff00, + side: THREE.DoubleSide, + opacity: 0.2, + transparent: true, + }), ); const [x, y, z] = topLeftOfEdge; plane.position.set(x, y, z); plane.userData.boxId = this.id; plane.userData.edgeId = edgeId; + plane.userData.cube = this; return plane; } @@ -173,27 +201,33 @@ class Cube { const { max } = this; const width = max[0] - min[0]; const height = max[1] - min[1]; - return [ + const planes = [ this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 1, 0), this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 1), this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 1, 2), this.getHitPlane(width, height, [min[0], max[1], min[2]], "width", 0, 3), ]; + planes.forEach(plane => { + plane.userData.plane = OrthoViews.PLANE_XY; + }); + return planes; } getYZPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { const { min } = this; const { max } = this; - const width = max[1] - min[1]; - const height = max[2] - min[2]; + const width = max[2] - min[2]; + const height = max[1] - min[1]; const planes = [ - this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 2, 4), - this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 1, 5), - this.getHitPlane(width, height, [min[0], max[1], min[2]], "height", 2, 6), - this.getHitPlane(width, height, [min[0], min[1], max[2]], "width", 1, 7), + this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 1, 0), + this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 2, 1), + this.getHitPlane(width, height, [min[0], min[1], max[2]], "height", 1, 2), + this.getHitPlane(width, height, [min[0], max[1], min[2]], "width", 2, 3), ]; planes.forEach(plane => { + // Rotating to the correct orientation. plane.geometry.rotateY(Math.PI / 2); + plane.userData.plane = OrthoViews.PLANE_YZ; }); return planes; } @@ -204,13 +238,16 @@ class Cube { const width = max[0] - min[0]; const height = max[2] - min[2]; const planes = [ - this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 2, 8), - this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 9), - this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 2, 10), - this.getHitPlane(width, height, [min[0], min[1], max[2]], "width", 0, 11), + // TODO: Test if all sphere intersections fail or why the object is pushed / not pushed to the array!! + this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 2, 0), + this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 1), + this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 2, 2), + this.getHitPlane(width, height, [min[0], min[1], max[2]], "width", 0, 3), ]; planes.forEach(plane => { + // Rotating to the correct orientation. plane.geometry.rotateX(Math.PI / 2); + plane.userData.plane = OrthoViews.PLANE_XZ; }); return planes; } @@ -284,31 +321,9 @@ class Cube { } createEdgeHitboxesForCrossSections() { - // The bounding boxes for the edges of the cube have an id equal to the index of the array. - // The index of the edge is illustrated in the following ascii art. - // min--> +-----1------+ - // .'| .'| - // 0' 4 2' | - // .' | .' 5 - // +----3-------+ | - // | | | | - // | +---9-|------+ - // 7 .' 6 .' - // | .8 | 10 - // |.' | .' - // +----11------+' <-- max - // - // -z - // ▵ ↗ -y - // | .' - // | .' - // |.' - // ----------> +x - // this.crossSectionHitPlanes[OrthoViews.PLANE_XY] = this.getXYPlaneCrossSectionHitPlanes(); - this.crossSectionHitPlanes[OrthoViews.PLANE_XY] = []; - this.crossSectionHitPlanes[OrthoViews.PLANE_YZ] = []; + this.crossSectionHitPlanes[OrthoViews.PLANE_XY] = this.getXYPlaneCrossSectionHitPlanes(); + this.crossSectionHitPlanes[OrthoViews.PLANE_YZ] = this.getYZPlaneCrossSectionHitPlanes(); this.crossSectionHitPlanes[OrthoViews.PLANE_XZ] = this.getXZPlaneCrossSectionHitPlanes(); - // this.crossSectionHitPlanes[OrthoViews.PLANE_XZ] = []; } updatePosition(position: Vector3) { @@ -327,13 +342,14 @@ class Cube { geometry.computeBoundingSphere(); geometry.verticesNeedUpdate = true; + const offset = planeId === OrthoViews.PLANE_XY ? +0.001 : -0.001; if (position[thirdDim] >= this.min[thirdDim] && position[thirdDim] < this.max[thirdDim]) { for (const hitPlane of this.crossSectionHitPlanes[planeId]) { const hitPlanePosition = hitPlane.position.toArray(); - hitPlanePosition[thirdDim] = position[thirdDim]; + // TODO: Offset!!!!!!! + hitPlanePosition[thirdDim] = position[thirdDim] + offset; hitPlane.position.set(hitPlanePosition[0], hitPlanePosition[1], hitPlanePosition[2]); hitPlane.geometry.verticesNeedUpdate = true; - console.log(hitPlane); // hitPlane.geometry.attributes.position.needsUpdate = true; // TODO: "The bounding sphere is also computed automatically when doing raycasting." // According to this, the call can be removed and will then only be done when needed. diff --git a/frontend/javascripts/oxalis/view/plane_view.js b/frontend/javascripts/oxalis/view/plane_view.js index e2f0d3dcb6d..7913c83d83d 100644 --- a/frontend/javascripts/oxalis/view/plane_view.js +++ b/frontend/javascripts/oxalis/view/plane_view.js @@ -165,23 +165,18 @@ class PlaneView { (mousePosition[0] / viewport.width) * 2 - 1, ((mousePosition[1] / viewport.height) * 2 - 1) * -1, // y is inverted ); - let camera = this.cameras[orthoView]; - if (clipAtDistanceZero) { + const camera = this.cameras[orthoView]; + /* if (clipAtDistanceZero) { camera = camera.clone(false); // Only pick what is being rendered in the ortho viewports. camera.far = 0.1; camera.updateProjectionMatrix(); - } + } */ raycaster.setFromCamera(mouse, camera); // The second parameter of intersectObjects is set to true to ensure that // the groups which contain the actual meshes are traversed. - const intersections = raycaster.intersectObjects(groupToTest.children, true); + const intersections = raycaster.intersectObjects(groupToTest.children, false); const intersection = intersections.length > 0 ? intersections[0] : null; - /* intersections.forEach(({ object: hitObject }) => { - hitObject.material.color.r = 1; - hitObject.material.color.g = 0; - hitObject.material.color.b = 0; - }); */ return intersection; } @@ -239,7 +234,7 @@ class PlaneView { } } - performBoundingBoxHitTest(mousePosition: [number, number]): ?typeof THREE.Vector3 { + performBoundingBoxHitTest(mousePosition: [number, number]): ?typeof THREE.Mesh { const storeState = Store.getState(); const { activeViewport } = storeState.viewModeData.plane; // Currently, the bounding box tool only supports the 2d viewports. @@ -256,45 +251,47 @@ class PlaneView { activeViewport, true, ); - console.log(intersection); const hitObject = intersection != null ? intersection.object : null; // Check whether we are hitting the same object as before, since we can return early // in this case. - if (hitObject === this.lastBoundingBoxHit) { + const didHitSamePlane = + hitObject != null && + this.lastBoundingBoxHit != null && + this.lastBoundingBoxHit.userData.plane === hitObject.userData.plane; + + if (hitObject === this.lastBoundingBoxHit || didHitSamePlane) { if (hitObject != null) { console.log("Hit the same object"); } - return intersection != null ? intersection.point : null; + this.lastBoundingBoxHit = hitObject; + return hitObject; } - console.log(hitObject); + // Undo highlighting of old hit if (this.lastBoundingBoxHit != null) { // Get HSL, save in userData, light the hsl up and set the new color. // changing emissive doesnt work for this material. - this.lastBoundingBoxHit.material.color.r = 0; - this.lastBoundingBoxHit.material.color.g = 1; - this.lastBoundingBoxHit.material.color.b = 0; + const { userData } = this.lastBoundingBoxHit; + const previousHighlightedCrossection = userData.cube.crossSections[userData.plane]; + previousHighlightedCrossection.material.color.r = 0; + previousHighlightedCrossection.material.color.g = 1; + previousHighlightedCrossection.material.color.b = 0; } this.lastBoundingBoxHit = hitObject; // Highlight new hit if (hitObject != null) { - // debugger; // TODO: gucken warum sich die Farbe der BBoxen nicht ändert. Scheint das falsche intersection object zu sein!!!! - const hslColor = { h: 0, s: 0, l: 0 }; - // const hoveredColor = [0.7, 0.5, 0.1]; - hitObject.material.color.getHSL(hslColor); - hitObject.userData.originalColor = hslColor; // const lightenedColor = { h: hslColor.h, s: 1, l: 1 }; - // hitObject.material.color.setHSL(lightenedColor); - // hitObject.material.color.setHSL(...hoveredColor); - hitObject.material.color.r = 1; - hitObject.material.color.g = 0; - hitObject.material.color.b = 0; - // (...hoveredColor); + const { userData } = hitObject; + const newHighlightedCrosssection = userData.cube.crossSections[userData.plane]; + newHighlightedCrosssection.material.color.r = 1; + newHighlightedCrosssection.material.color.g = 0; + newHighlightedCrosssection.material.color.b = 0; } - return null; + this.draw(); + return hitObject; } draw(): void { From 49f9d0077568b06c2e96c4687872f0802e5ddb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 14 Oct 2021 16:04:24 +0200 Subject: [PATCH 05/34] Use own hovered hitbox detection and enable bbox resizing --- .../combinations/bounding_box_handlers.js | 38 +++++----- .../controller/combinations/tool_controls.js | 73 ++++++++----------- .../oxalis/controller/scene_controller.js | 33 ++++++--- .../javascripts/oxalis/geometries/cube.js | 42 ++++++++--- .../model/accessors/view_mode_accessor.js | 15 ++-- 5 files changed, 116 insertions(+), 85 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index 2cfe6e5c959..d77b341c1c7 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -19,7 +19,7 @@ const neighbourEdgeIndexByEdgeIndex = { "2": [0, 1], "3": [0, 1], }; -const MAX_DISTANCE_TO_SELECTION = 40; +const MAX_DISTANCE_TO_SELECTION = 15; function getDistanceToBoundingBoxEdge( pos: Vector3, @@ -47,34 +47,34 @@ function getDistanceToBoundingBoxEdge( // // This example is for the xy viewport for x as the main direction / edgeDim. const cornerToCompareWith = compareToMin ? min : max; - if (pos[otherDim] < min[otherDim]) { - // Case 1: Distance to the min corner is needed in otherDim. + if (pos[edgeDim] < min[edgeDim]) { + // Case 1: Distance to the min corner is needed in edgeDim. return Math.sqrt( - Math.abs(pos[edgeDim] - cornerToCompareWith[edgeDim]) ** 2 + - Math.abs(pos[otherDim] - min[otherDim]) ** 2, + Math.abs(pos[edgeDim] - min[edgeDim]) ** 2 + + Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, ); } - if (pos[otherDim] > max[otherDim]) { - // Case 2: Distance to max Corner is needed in otherDim. + if (pos[edgeDim] > max[edgeDim]) { + // Case 2: Distance to max Corner is needed in edgeDim. return Math.sqrt( - Math.abs(pos[edgeDim] - cornerToCompareWith[edgeDim]) ** 2 + - Math.abs(pos[otherDim] - max[otherDim]) ** 2, + Math.abs(pos[edgeDim] - max[edgeDim]) ** 2 + + Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, ); } // Case 3: - // If the position is within the bounds of the other dimension, the shortest distance - // to the edge is simply the difference between the edgeDim values. - return Math.abs(pos[edgeDim] - cornerToCompareWith[edgeDim]); + // If the position is within the bounds of the edgeDim, the shortest distance + // to the edge is simply the difference between the otherDim values. + return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]); } export default function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView) { const state = Store.getState(); - const globalPosition = calculateGlobalPos(state, pos); + const globalPosition = calculateGlobalPos(state, pos, plane); const { userBoundingBoxes } = getSomeTracing(state.tracing); const reorderedIndices = Dimension.getIndices(plane); const thirdDim = reorderedIndices[2]; - let currentNearestDistance = MAX_DISTANCE_TO_SELECTION; + let currentNearestDistance = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep; let currentNearestBoundingBox = null; let currentNearestDistanceArray = null; @@ -83,7 +83,7 @@ export default function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoVi const isCrossSectionOfViewportVisible = globalPosition[thirdDim] >= min[thirdDim] && globalPosition[thirdDim] < max[thirdDim]; if (!isCrossSectionOfViewportVisible) { - break; + continue; } const distanceArray = [ getDistanceToBoundingBoxEdge( @@ -129,18 +129,18 @@ export default function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoVi if (currentNearestBoundingBox == null || currentNearestDistanceArray == null) { return null; } - // TODO: nearestEdgeIndex seems wrong. should be 0 or 1 instead of 2 or 3 and other way round. - // maybe a little logic error in getDistanceToBoundingBoxEdge or when calling this method. const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance); const dimensionOfNearestEdge = nearestEdgeIndex < 2 ? reorderedIndices[0] : reorderedIndices[1]; - const edgeDirection = nearestEdgeIndex < 2 ? "vertical" : "horizontal"; + const direction = nearestEdgeIndex < 2 ? "horizontal" : "vertical"; const isMaxEdge = nearestEdgeIndex % 2 === 1; + const resizableDimension = nearestEdgeIndex < 2 ? reorderedIndices[1] : reorderedIndices[0]; // TODO: Add feature to select corners. return { boxId: currentNearestBoundingBox.id, dimensionIndex: dimensionOfNearestEdge, - edgeDirection, + direction, isMaxEdge, nearestEdgeIndex, + resizableDimension, }; } diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 7e83caa2817..c43b0f77c81 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -17,8 +17,8 @@ import { import { handleAgglomerateSkeletonAtClick } from "oxalis/controller/combinations/segmentation_handlers"; import { hideBrushAction } from "oxalis/model/actions/volumetracing_actions"; import { isBrushTool } from "oxalis/model/accessors/tool_accessor"; +import getSceneController from "oxalis/controller/scene_controller_provider"; import * as MoveHandlers from "oxalis/controller/combinations/move_handlers"; -import { edgeIdToEdge } from "oxalis/geometries/cube"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import PlaneView from "oxalis/view/plane_view"; import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers"; @@ -533,7 +533,29 @@ export class BoundingBoxTool { planeView: PlaneView, showNodeContextMenuAt: ShowContextMenuFunction, ): * { + const bboxHoveringThrottleTime = 75; let selectedEdge = null; + const getClosestHoveredBoundingBoxThrottled = + planeId !== OrthoViews.TDView + ? _.throttle((delta: Point2, position: Point2, id, event) => { + const { body } = document; + if (body == null || selectedEdge != null) { + return; + } + const newSelectedEdge = getClosestHoveredBoundingBox(position, planeId); + if (newSelectedEdge != null) { + getSceneController().highlightUserBoundingBox(newSelectedEdge.boxId); + if (newSelectedEdge.direction === "horizontal") { + body.style.cursor = "row-resize"; + } else { + body.style.cursor = "col-resize"; + } + } else { + getSceneController().highlightUserBoundingBox(null); + body.style.cursor = "default"; + } + }, bboxHoveringThrottleTime) + : () => {}; return { leftDownMove: (delta: Point2, _pos: Point2, _id: ?string, _event: MouseEvent) => { if (selectedEdge != null) { @@ -541,26 +563,19 @@ export class BoundingBoxTool { const currentlySelectedEdge = selectedEdge; // avoiding flow complains. const { userBoundingBoxes } = getSomeTracing(state.tracing); const zoomFactor = state.flycam.zoomStep; - // TODO: Discussion aufschreiben: - // Raycaster nutzt gpu nicht -> nicht all zu fix wie gedacht. - // Raycaster benötigt zusätzlich noch weitere objekte, die immer mitgeupdatet werden müssen (spätestens wenn das bbox tool an ist) - // Manuelles Testen wäre nicht schlauer gemacht / schneller - - // Generelle Frage: Was wenn man mehrere BBox kandidaten hat, beim hoveren? eine eigene berechnung wäre da fixer: - // Pro Bounding Box checken nur ein check nötig, da die plane explizit bekannt ist, const scaleFactor = getBaseVoxelFactors(state.dataset.dataSource.scale); const updatedUserBoundingBoxes = userBoundingBoxes.map(bbox => { if (bbox.id !== currentlySelectedEdge.boxId) { return bbox; } bbox = _.cloneDeep(bbox); + const { resizableDimension } = currentlySelectedEdge; // For a horizontal edge only consider delta.y, for vertical only delta.x const movement = currentlySelectedEdge.direction === "horizontal" ? delta.y : delta.x; const minOrMax = currentlySelectedEdge.isMaxEdge ? "max" : "min"; - const scaledMovement = - movement * zoomFactor * scaleFactor[currentlySelectedEdge.dimensionIndex]; - bbox.boundingBox[minOrMax][currentlySelectedEdge.dimensionIndex] = Math.round( - bbox.boundingBox[minOrMax][currentlySelectedEdge.dimensionIndex] + scaledMovement, + const scaledMovement = movement * zoomFactor * scaleFactor[resizableDimension]; + bbox.boundingBox[minOrMax][resizableDimension] = Math.round( + bbox.boundingBox[minOrMax][resizableDimension] + scaledMovement, ); return bbox; }); @@ -571,18 +586,15 @@ export class BoundingBoxTool { } }, leftMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { - const hitObject = planeView.performBoundingBoxHitTest([pos.x, pos.y]); - if (hitObject != null) { - const { userData } = hitObject; - selectedEdge = { - boxId: userData.boxId, - ...edgeIdToEdge(userData.edgeId, userData.plane), - }; + selectedEdge = getClosestHoveredBoundingBox(pos, planeId); + if (selectedEdge) { + getSceneController().highlightUserBoundingBox(selectedEdge.boxId); } }, leftMouseUp: () => { selectedEdge = null; + getSceneController().highlightUserBoundingBox(null); }, rightDownMove: (delta: Point2, pos: Point2) => { @@ -596,28 +608,7 @@ export class BoundingBoxTool { rightMouseUp: () => { console.log("BoundingBox tool right up"); }, - mouseMove: (delta: Point2, position: Point2, id, event) => { - const { body } = document; - if (body == null || selectedEdge != null) { - return; - } - const info = getClosestHoveredBoundingBox(position, planeId); - console.log("info", info); - // planeView.performIsosurfaceHitTest([position.x, position.y]); - /* const hitObject = planeView.throttledPerformBoundingBoxHitTest([position.x, position.y]); - if (hitObject != null) { - const { userData } = hitObject; - const { direction } = edgeIdToEdge(userData.edgeId, userData.plane); - if (direction === "horizontal") { - body.style.cursor = "row-resize"; - } else { - body.style.cursor = "col-resize"; - } - } else { - body.style.cursor = "default"; - } */ - }, - + mouseMove: getClosestHoveredBoundingBoxThrottled, leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { /* const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 81c4543668b..26de26f0f72 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -53,8 +53,8 @@ class SceneController { planeShift: Vector3; datasetBoundingBox: Cube; userBoundingBoxGroup: typeof THREE.Group; - userBoundingBoxHitPlanesGroup: typeof THREE.Group; userBoundingBoxes: Array; + highlightedBBoxId: ?number; taskBoundingBox: ?Cube; contour: ContourGeometry; planes: OrthoViewMap; @@ -100,6 +100,7 @@ class SceneController { this.rootGroup.add(this.getRootNode()); this.isosurfacesRootGroup = new THREE.Group(); this.meshesRootGroup = new THREE.Group(); + this.highlightedBBoxId = null; // The dimension(s) with the highest resolution will not be distorted this.rootGroup.scale.copy(new THREE.Vector3(...Store.getState().dataset.dataSource.scale)); @@ -276,7 +277,6 @@ class SceneController { createMeshes(): void { this.rootNode = new THREE.Object3D(); this.userBoundingBoxGroup = new THREE.Group(); - this.userBoundingBoxHitPlanesGroup = new THREE.Group(); this.rootNode.add(this.userBoundingBoxGroup); this.userBoundingBoxes = []; @@ -287,6 +287,7 @@ class SceneController { max: upperBoundary, color: CUBE_COLOR, showCrossSections: true, + isHighlighted: false, }); this.datasetBoundingBox.getMeshes().forEach(mesh => this.rootNode.add(mesh)); @@ -333,6 +334,7 @@ class SceneController { max: taskBoundingBox.max, color: 0x00ff00, showCrossSections: true, + isHighlighted: false, }); this.taskBoundingBox.getMeshes().forEach(mesh => this.rootNode.add(mesh)); if (constants.MODES_ARBITRARY.includes(viewMode)) { @@ -451,7 +453,6 @@ class SceneController { setUserBoundingBoxes(bboxes: Array): void { const newUserBoundingBoxGroup = new THREE.Group(); - const newUserBoundingBoxHitPlanesGroup = new THREE.Group(); this.userBoundingBoxes = bboxes.map(({ boundingBox, isVisible, color, id }) => { const { min, max } = boundingBox; const bbColor = [color[0] * 255, color[1] * 255, color[2] * 255]; @@ -462,23 +463,37 @@ class SceneController { showCrossSections: true, id, isEditable: true, + isHighlighted: this.highlightedBBoxId === id, }); bbCube.setVisibility(isVisible); bbCube.getMeshes().forEach(mesh => { newUserBoundingBoxGroup.add(mesh); mesh.userData.id = id; }); - bbCube - .getCrossSectionHitPlanes() - .forEach(hitPlane => newUserBoundingBoxHitPlanesGroup.add(hitPlane)); return bbCube; }); this.rootNode.remove(this.userBoundingBoxGroup); - this.rootNode.remove(this.userBoundingBoxHitPlanesGroup); this.userBoundingBoxGroup = newUserBoundingBoxGroup; - this.userBoundingBoxHitPlanesGroup = newUserBoundingBoxHitPlanesGroup; this.rootNode.add(this.userBoundingBoxGroup); - this.rootNode.add(this.userBoundingBoxHitPlanesGroup); + } + + highlightUserBoundingBox(bboxId: ?number): void { + if (this.highlightedBBoxId === bboxId) { + return; + } + const highlightOrUnhighlightUserBBox = (id: number, highlight: boolean) => { + const bboxToChangeHighlighting = this.userBoundingBoxes.find(bbCube => bbCube.id === id); + if (bboxToChangeHighlighting != null) { + bboxToChangeHighlighting.setIsHighlighted(highlight); + } + }; + if (this.highlightedBBoxId != null) { + highlightOrUnhighlightUserBBox(this.highlightedBBoxId, false); + } + if (bboxId != null) { + highlightOrUnhighlightUserBBox(bboxId, true); + } + this.highlightedBBoxId = bboxId; } setSkeletonGroupVisibility(isVisible: boolean) { diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index b31884cc80e..c22265d143f 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -28,6 +28,7 @@ type Properties = { showCrossSections?: boolean, id?: number, isEditable?: boolean, + isHighlighted: boolean, }; type PlaneGeometry = typeof THREE.PlaneGeometry; @@ -68,6 +69,9 @@ class Cube { visible: boolean; id: ?number; isEditable: boolean; + lineWidth: number; + color: number; + isHighlighted: boolean; constructor(properties: Properties) { // min/max should denote a half-open interval. @@ -75,16 +79,15 @@ class Cube { this.max = properties.max; this.id = properties.id; this.isEditable = properties.isEditable || false; - const lineWidth = properties.lineWidth != null ? properties.lineWidth : 1; - const color = properties.color || 0x000000; + this.lineWidth = properties.lineWidth != null ? properties.lineWidth : 1; + this.color = properties.color || 0x000000; this.showCrossSections = properties.showCrossSections || false; this.initialized = false; this.visible = true; + this.isHighlighted = properties.isHighlighted; - const lineProperties = { color, linewidth: lineWidth }; - - this.cube = new THREE.Line(new THREE.Geometry(), new THREE.LineBasicMaterial(lineProperties)); + this.cube = new THREE.Line(new THREE.Geometry(), this.getLineMaterial()); this.crossSections = {}; this.crossSectionHitPlanes = { @@ -93,10 +96,7 @@ class Cube { [OrthoViews.PLANE_XZ]: [], }; for (const planeId of OrthoViewValuesWithoutTDView) { - this.crossSections[planeId] = new THREE.Line( - new THREE.Geometry(), - new THREE.LineBasicMaterial(lineProperties), - ); + this.crossSections[planeId] = new THREE.Line(new THREE.Geometry(), this.getLineMaterial()); } if (this.min != null && this.max != null) { @@ -109,6 +109,18 @@ class Cube { ); } + getLineMaterial() { + return this.isHighlighted + ? new THREE.LineDashedMaterial({ + color: 0xffffff, + linewidth: this.lineWidth, + scale: 1, + dashSize: 5, + gapSize: 5, + }) + : new THREE.LineBasicMaterial({ color: this.color, linewidth: this.lineWidth }); + } + getEdgeHitBox( width: number, height: number, @@ -312,6 +324,7 @@ class Cube { for (const mesh of _.values(this.crossSections).concat([this.cube])) { mesh.geometry.computeBoundingSphere(); + mesh.computeLineDistances(); mesh.geometry.verticesNeedUpdate = true; } @@ -367,6 +380,17 @@ class Cube { return _.flattenDeep(_.values(this.crossSectionHitPlanes)); } + setIsHighlighted(highlighted: boolean) { + if (highlighted === this.isHighlighted) { + return; + } + this.isHighlighted = highlighted; + this.getMeshes().forEach(mesh => { + mesh.material = this.getLineMaterial(); + }); + app.vent.trigger("rerender"); + } + updateForCam(id: OrthoView) { if (!this.initialized) { return; diff --git a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js index da0a12fa884..46af12f2aca 100644 --- a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js @@ -10,6 +10,7 @@ import constants, { type Rect, type Viewport, OrthoViews, + type OrthoView, type Point2, type Vector3, type ViewMode, @@ -77,7 +78,7 @@ export function getInputCatcherAspectRatio(state: OxalisState, viewport: Viewpor // // example I: // // 16 : 9 --> 1.78 -// // useWdth: false +// // useWidth: false // const useWidth = false; // const scaledWidth = width * (useWidth ? 1 : aspectRatio); @@ -97,19 +98,19 @@ export function getViewportScale(state: OxalisState, viewport: Viewport): [numbe return [xScale, yScale]; } -function _calculateGlobalPos(state: OxalisState, clickPos: Point2): Vector3 { +function _calculateGlobalPos(state: OxalisState, clickPos: Point2, planeId: ?OrthoView): Vector3 { let position; - const { activeViewport } = state.viewModeData.plane; + planeId = planeId || state.viewModeData.plane.activeViewport; const curGlobalPos = getPosition(state.flycam); - const zoomFactors = getPlaneScalingFactor(state, state.flycam, activeViewport); - const viewportScale = getViewportScale(state, activeViewport); + const zoomFactors = getPlaneScalingFactor(state, state.flycam, planeId); + const viewportScale = getViewportScale(state, planeId); const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); const center = [0, 1].map(dim => (constants.VIEWPORT_WIDTH * viewportScale[dim]) / 2); const diffX = ((center[0] - clickPos.x) / viewportScale[0]) * zoomFactors[0]; const diffY = ((center[1] - clickPos.y) / viewportScale[1]) * zoomFactors[1]; - switch (activeViewport) { + switch (planeId) { case OrthoViews.PLANE_XY: position = [ curGlobalPos[0] - diffX * planeRatio[0], @@ -133,7 +134,7 @@ function _calculateGlobalPos(state: OxalisState, clickPos: Point2): Vector3 { break; default: console.error( - `Trying to calculate the global position, but no viewport is active: ${activeViewport}`, + `Trying to calculate the global position, but no viewport is active: ${planeId}`, ); return [0, 0, 0]; } From 2830eb78aab6a25b6cc35250743b78553a22425a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 14 Oct 2021 17:06:14 +0200 Subject: [PATCH 06/34] Remove unused code, fix linting and flow --- .../combinations/bounding_box_handlers.js | 5 +- .../controller/combinations/tool_controls.js | 16 +- .../oxalis/controller/scene_controller.js | 5 +- .../javascripts/oxalis/geometries/cube.js | 205 ------------------ .../javascripts/oxalis/view/plane_view.js | 141 +++--------- 5 files changed, 39 insertions(+), 333 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index d77b341c1c7..1c0efac3658 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -5,7 +5,8 @@ import Store from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import Dimension from "oxalis/model/dimensions"; -const neighbourEdgeIndexByEdgeIndex = { +/* const neighbourEdgeIndexByEdgeIndex = { + // TODO: Use this to detect corners properly. // The edges are indexed within the plane like this: // See the distanceArray calculation as a reference. // +---0---+ @@ -18,7 +19,7 @@ const neighbourEdgeIndexByEdgeIndex = { "1": [2, 3], "2": [0, 1], "3": [0, 1], -}; +}; */ const MAX_DISTANCE_TO_SELECTION = 15; function getDistanceToBoundingBoxEdge( diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index c43b0f77c81..2f81c47f5d8 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -530,14 +530,14 @@ export class FillCellTool { export class BoundingBoxTool { static getPlaneMouseControls( planeId: OrthoView, - planeView: PlaneView, - showNodeContextMenuAt: ShowContextMenuFunction, + _planeView: PlaneView, + _showNodeContextMenuAt: ShowContextMenuFunction, ): * { const bboxHoveringThrottleTime = 75; let selectedEdge = null; const getClosestHoveredBoundingBoxThrottled = planeId !== OrthoViews.TDView - ? _.throttle((delta: Point2, position: Point2, id, event) => { + ? _.throttle((delta: Point2, position: Point2, _id, _event) => { const { body } = document; if (body == null || selectedEdge != null) { return; @@ -585,7 +585,7 @@ export class BoundingBoxTool { MoveHandlers.handleMovePlane(delta); } }, - leftMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + leftMouseDown: (pos: Point2, _plane: OrthoView, _event: MouseEvent) => { selectedEdge = getClosestHoveredBoundingBox(pos, planeId); if (selectedEdge) { getSceneController().highlightUserBoundingBox(selectedEdge.boxId); @@ -597,11 +597,11 @@ export class BoundingBoxTool { getSceneController().highlightUserBoundingBox(null); }, - rightDownMove: (delta: Point2, pos: Point2) => { + rightDownMove: (_delta: Point2, _pos: Point2) => { console.log("BoundingBox tool right down move"); }, - rightMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + rightMouseDown: (_pos: Point2, _plane: OrthoView, _event: MouseEvent) => { console.log("BoundingBox tool right down"); }, @@ -609,7 +609,7 @@ export class BoundingBoxTool { console.log("BoundingBox tool right up"); }, mouseMove: getClosestHoveredBoundingBoxThrottled, - leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { + leftClick: (_pos: Point2, _plane: OrthoView, _event: MouseEvent) => { /* const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); const boundingBoxId = highestBoundingBoxId + 1; @@ -624,7 +624,7 @@ export class BoundingBoxTool { Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); */ }, - rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { + rightClick: (_pos: Point2, _plane: OrthoView, _event: MouseEvent, _isTouch: boolean) => { console.log("BoundingBox right click"); }, }; diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 26de26f0f72..7296b196def 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -466,10 +466,7 @@ class SceneController { isHighlighted: this.highlightedBBoxId === id, }); bbCube.setVisibility(isVisible); - bbCube.getMeshes().forEach(mesh => { - newUserBoundingBoxGroup.add(mesh); - mesh.userData.id = id; - }); + bbCube.getMeshes().forEach(mesh => newUserBoundingBoxGroup.add(mesh)); return bbCube; }); this.rootNode.remove(this.userBoundingBoxGroup); diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index c22265d143f..cc3b7d9fd33 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -15,7 +15,6 @@ import { type Vector3, } from "oxalis/constants"; import { getPosition } from "oxalis/model/accessors/flycam_accessor"; -import ErrorHandling from "libs/error_handling"; import Store from "oxalis/throttled_store"; import app from "app"; import dimensions from "oxalis/model/dimensions"; @@ -31,35 +30,8 @@ type Properties = { isHighlighted: boolean, }; -type PlaneGeometry = typeof THREE.PlaneGeometry; -// type CrossSectionHitPlanesTuple = [PlaneGeometry, PlaneGeometry, PlaneGeometry, PlaneGeometry]; -type CrossSectionHitPlanesTuple = Array; - -export function edgeIdToEdge(id: number, plane: OrthoView) { - const isMaxEdge = id > 1; - const direction = id % 2 === 0 ? "vertical" : "horizontal"; - switch (plane) { - case OrthoViews.PLANE_XY: { - const dimensionIndex = id % 2 === 0 ? 0 : 1; - return { dimensionIndex, direction, isMaxEdge }; - } - case OrthoViews.PLANE_YZ: { - const dimensionIndex = id % 2 === 0 ? 2 : 1; - return { dimensionIndex, direction, isMaxEdge }; - } - case OrthoViews.PLANE_XZ: { - const dimensionIndex = id % 2 === 0 ? 0 : 2; - return { dimensionIndex, direction, isMaxEdge }; - } - default: { - return { dimensionIndex: 0, direction, isMaxEdge }; - } - } -} - class Cube { crossSections: OrthoViewMap; - crossSectionHitPlanes: OrthoViewMap; cube: typeof THREE.Line; min: Vector3; @@ -90,11 +62,6 @@ class Cube { this.cube = new THREE.Line(new THREE.Geometry(), this.getLineMaterial()); this.crossSections = {}; - this.crossSectionHitPlanes = { - [OrthoViews.PLANE_XY]: [], - [OrthoViews.PLANE_YZ]: [], - [OrthoViews.PLANE_XZ]: [], - }; for (const planeId of OrthoViewValuesWithoutTDView) { this.crossSections[planeId] = new THREE.Line(new THREE.Geometry(), this.getLineMaterial()); } @@ -121,149 +88,6 @@ class Cube { : new THREE.LineBasicMaterial({ color: this.color, linewidth: this.lineWidth }); } - getEdgeHitBox( - width: number, - height: number, - depth: number, - x: number, - y: number, - z: number, - direction: string, - edgeId: number, - ): typeof THREE.BoxGeometry { - const maxWidth = 40; - let geometry; - switch (direction) { - case "width": { - geometry = new THREE.BoxGeometry( - width + maxWidth, - Math.min(maxWidth, height / 3), - Math.min(maxWidth, depth / 3), - ); - break; - } - case "depth": { - geometry = new THREE.BoxGeometry( - Math.min(maxWidth, width / 3), - Math.min(maxWidth, height / 3), - depth + maxWidth, - ); - break; - } - case "height": { - geometry = new THREE.BoxGeometry( - Math.min(maxWidth, width / 3), - height + maxWidth, - Math.min(maxWidth, depth / 3), - ); - break; - } - default: { - geometry = new THREE.BoxGeometry(1, 1, 1); - } - } - const box = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: 0x00ff00 })); - box.position.set(x, y, z); - box.userData.boxId = this.id; - box.userData.edgeId = edgeId; - return box; - } - - getHitPlane( - crossSectionWidth: number, - crossSectionHeight: number, - topLeftOfEdge: [number, number, number], - direction: "width" | "height", - extendDirectionIndex: 0 | 1 | 2, - edgeId: number, - ): typeof THREE.PlaneGeometry { - const maxHitOffset = 40; - let planeWidth; - let planeHeight; - if (direction === "width") { - planeWidth = crossSectionWidth + maxHitOffset; - planeHeight = Math.min(maxHitOffset, crossSectionHeight / 3); - topLeftOfEdge[extendDirectionIndex] += crossSectionWidth / 2; - } else { - planeWidth = Math.min(maxHitOffset, crossSectionWidth / 3); - planeHeight = crossSectionHeight + maxHitOffset; - topLeftOfEdge[extendDirectionIndex] += crossSectionHeight / 2; - } - const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight); - // TODO: Adjust Orientation of the planes according to the plane parameter!!!! - const plane = new THREE.Mesh( - geometry, - new THREE.MeshBasicMaterial({ - color: 0x00ff00, - side: THREE.DoubleSide, - opacity: 0.2, - transparent: true, - }), - ); - const [x, y, z] = topLeftOfEdge; - plane.position.set(x, y, z); - plane.userData.boxId = this.id; - plane.userData.edgeId = edgeId; - plane.userData.cube = this; - return plane; - } - - getXYPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { - const { min } = this; - const { max } = this; - const width = max[0] - min[0]; - const height = max[1] - min[1]; - const planes = [ - this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 1, 0), - this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 1), - this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 1, 2), - this.getHitPlane(width, height, [min[0], max[1], min[2]], "width", 0, 3), - ]; - planes.forEach(plane => { - plane.userData.plane = OrthoViews.PLANE_XY; - }); - return planes; - } - - getYZPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { - const { min } = this; - const { max } = this; - const width = max[2] - min[2]; - const height = max[1] - min[1]; - const planes = [ - this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 1, 0), - this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 2, 1), - this.getHitPlane(width, height, [min[0], min[1], max[2]], "height", 1, 2), - this.getHitPlane(width, height, [min[0], max[1], min[2]], "width", 2, 3), - ]; - planes.forEach(plane => { - // Rotating to the correct orientation. - plane.geometry.rotateY(Math.PI / 2); - plane.userData.plane = OrthoViews.PLANE_YZ; - }); - return planes; - } - - getXZPlaneCrossSectionHitPlanes(): CrossSectionHitPlanesTuple { - const { min } = this; - const { max } = this; - const width = max[0] - min[0]; - const height = max[2] - min[2]; - const planes = [ - // TODO: Test if all sphere intersections fail or why the object is pushed / not pushed to the array!! - this.getHitPlane(width, height, [min[0], min[1], min[2]], "height", 2, 0), - this.getHitPlane(width, height, [min[0], min[1], min[2]], "width", 0, 1), - this.getHitPlane(width, height, [max[0], min[1], min[2]], "height", 2, 2), - this.getHitPlane(width, height, [min[0], min[1], max[2]], "width", 0, 3), - ]; - planes.forEach(plane => { - // Rotating to the correct orientation. - plane.geometry.rotateX(Math.PI / 2); - plane.userData.plane = OrthoViews.PLANE_XZ; - }); - return planes; - } - setCorners(min: Vector3, max: Vector3) { this.min = min; this.max = max; @@ -317,11 +141,6 @@ class Cube { vec(min[0], 0, min[2]), ]; - if (this.isEditable) { - ErrorHandling.assert(this.id != null, "Every editable bounding box needs an id!"); - this.createEdgeHitboxesForCrossSections(); - } - for (const mesh of _.values(this.crossSections).concat([this.cube])) { mesh.geometry.computeBoundingSphere(); mesh.computeLineDistances(); @@ -333,12 +152,6 @@ class Cube { app.vent.trigger("rerender"); } - createEdgeHitboxesForCrossSections() { - this.crossSectionHitPlanes[OrthoViews.PLANE_XY] = this.getXYPlaneCrossSectionHitPlanes(); - this.crossSectionHitPlanes[OrthoViews.PLANE_YZ] = this.getYZPlaneCrossSectionHitPlanes(); - this.crossSectionHitPlanes[OrthoViews.PLANE_XZ] = this.getXZPlaneCrossSectionHitPlanes(); - } - updatePosition(position: Vector3) { if (!this.initialized) { return; @@ -355,20 +168,6 @@ class Cube { geometry.computeBoundingSphere(); geometry.verticesNeedUpdate = true; - const offset = planeId === OrthoViews.PLANE_XY ? +0.001 : -0.001; - if (position[thirdDim] >= this.min[thirdDim] && position[thirdDim] < this.max[thirdDim]) { - for (const hitPlane of this.crossSectionHitPlanes[planeId]) { - const hitPlanePosition = hitPlane.position.toArray(); - // TODO: Offset!!!!!!! - hitPlanePosition[thirdDim] = position[thirdDim] + offset; - hitPlane.position.set(hitPlanePosition[0], hitPlanePosition[1], hitPlanePosition[2]); - hitPlane.geometry.verticesNeedUpdate = true; - // hitPlane.geometry.attributes.position.needsUpdate = true; - // TODO: "The bounding sphere is also computed automatically when doing raycasting." - // According to this, the call can be removed and will then only be done when needed. - hitPlane.geometry.computeBoundingBox(); - } - } } } @@ -376,10 +175,6 @@ class Cube { return [this.cube].concat(_.values(this.crossSections)); } - getCrossSectionHitPlanes(): Array { - return _.flattenDeep(_.values(this.crossSectionHitPlanes)); - } - setIsHighlighted(highlighted: boolean) { if (highlighted === this.isHighlighted) { return; diff --git a/frontend/javascripts/oxalis/view/plane_view.js b/frontend/javascripts/oxalis/view/plane_view.js index 7913c83d83d..2246bc1f592 100644 --- a/frontend/javascripts/oxalis/view/plane_view.js +++ b/frontend/javascripts/oxalis/view/plane_view.js @@ -12,9 +12,8 @@ import Constants, { type OrthoViewMap, OrthoViewValues, OrthoViews, - type OrthoView, } from "oxalis/constants"; -import Store, { type OxalisState } from "oxalis/store"; +import Store from "oxalis/store"; import app from "app"; import getSceneController from "oxalis/controller/scene_controller_provider"; import window from "libs/window"; @@ -31,7 +30,7 @@ const createDirLight = (position, target, intensity, parent) => { }; const raycaster = new THREE.Raycaster(); -raycaster.params.Line.threshold = 100; +let oldRaycasterHit = null; const ISOSURFACE_HOVER_THROTTLING_DELAY = 150; @@ -42,11 +41,8 @@ class PlaneView { cameras: OrthoViewMap; throttledPerformIsosurfaceHitTest: ([number, number]) => ?typeof THREE.Vector3; - throttledPerformBoundingBoxHitTest: ([number, number]) => ?typeof THREE.Vector3; running: boolean; - lastIsosurfaceHit: ?typeof THREE.Object3D; - lastBoundingBoxHit: ?typeof THREE.Object3D; needsRerender: boolean; constructor() { @@ -55,9 +51,7 @@ class PlaneView { this.performIsosurfaceHitTest, ISOSURFACE_HOVER_THROTTLING_DELAY, ); - this.throttledPerformBoundingBoxHitTest = _.throttle(this.performBoundingBoxHitTest, 75); - this.lastIsosurfaceHit = null; - this.lastBoundingBoxHit = null; + this.running = false; const { scene } = getSceneController(); @@ -152,39 +146,15 @@ class PlaneView { } } - performHitTestForSceneGroup( - storeState: OxalisState, - groupToTest: typeof THREE.Group, - mousePosition: [number, number], - orthoView: OrthoView, - clipAtDistanceZero: boolean, - ): ?typeof THREE.Intersection { - const viewport = getInputCatcherRect(storeState, orthoView); - // Perform ray casting - const mouse = new THREE.Vector2( - (mousePosition[0] / viewport.width) * 2 - 1, - ((mousePosition[1] / viewport.height) * 2 - 1) * -1, // y is inverted - ); - const camera = this.cameras[orthoView]; - /* if (clipAtDistanceZero) { - camera = camera.clone(false); - // Only pick what is being rendered in the ortho viewports. - camera.far = 0.1; - camera.updateProjectionMatrix(); - } */ - raycaster.setFromCamera(mouse, camera); - // The second parameter of intersectObjects is set to true to ensure that - // the groups which contain the actual meshes are traversed. - const intersections = raycaster.intersectObjects(groupToTest.children, false); - const intersection = intersections.length > 0 ? intersections[0] : null; - return intersection; - } - performIsosurfaceHitTest(mousePosition: [number, number]): ?typeof THREE.Vector3 { const storeState = Store.getState(); + const SceneController = getSceneController(); + const { isosurfacesRootGroup } = SceneController; + const tdViewport = getInputCatcherRect(storeState, "TDView"); + const { hoveredIsosurfaceId } = storeState.temporaryConfiguration; + // Outside of the 3D viewport, we don't do isosurface hit tests if (storeState.viewModeData.plane.activeViewport !== OrthoViews.TDView) { - const { hoveredIsosurfaceId } = storeState.temporaryConfiguration; if (hoveredIsosurfaceId !== 0) { // Reset hoveredIsosurfaceId if we are outside of the 3D viewport, // since that id takes precedence over the shader-calculated cell id @@ -194,106 +164,49 @@ class PlaneView { return null; } - const SceneController = getSceneController(); - const { isosurfacesRootGroup } = SceneController; - const intersection = this.performHitTestForSceneGroup( - storeState, - isosurfacesRootGroup, - mousePosition, - "TDView", - false, + // Perform ray casting + const mouse = new THREE.Vector2( + (mousePosition[0] / tdViewport.width) * 2 - 1, + ((mousePosition[1] / tdViewport.height) * 2 - 1) * -1, // y is inverted ); - const hitObject = intersection != null ? intersection.object : null; + + raycaster.setFromCamera(mouse, this.cameras[OrthoViews.TDView]); + // The second parameter of intersectObjects is set to true to ensure that + // the groups which contain the actual meshes are traversed. + const intersections = raycaster.intersectObjects(isosurfacesRootGroup.children, true); + const hitObject = intersections.length > 0 ? intersections[0].object : null; + // Check whether we are hitting the same object as before, since we can return early // in this case. - if (hitObject === this.lastIsosurfaceHit) { - return intersection != null ? intersection.point : null; + if (hitObject === oldRaycasterHit) { + return intersections.length > 0 ? intersections[0].point : null; } // Undo highlighting of old hit - if (this.lastIsosurfaceHit != null) { - this.lastIsosurfaceHit.parent.children.forEach(meshPart => { + if (oldRaycasterHit != null) { + oldRaycasterHit.parent.children.forEach(meshPart => { meshPart.material.emissive.setHex("#000000"); }); + oldRaycasterHit = null; } - this.lastIsosurfaceHit = hitObject; + oldRaycasterHit = hitObject; // Highlight new hit - if (hitObject != null && intersection != null) { + if (hitObject != null) { const hoveredColor = [0.7, 0.5, 0.1]; hitObject.parent.children.forEach(meshPart => { meshPart.material.emissive.setHSL(...hoveredColor); }); Store.dispatch(updateTemporarySettingAction("hoveredIsosurfaceId", hitObject.parent.cellId)); - return intersection.point; + return intersections[0].point; } else { Store.dispatch(updateTemporarySettingAction("hoveredIsosurfaceId", 0)); return null; } } - performBoundingBoxHitTest(mousePosition: [number, number]): ?typeof THREE.Mesh { - const storeState = Store.getState(); - const { activeViewport } = storeState.viewModeData.plane; - // Currently, the bounding box tool only supports the 2d viewports. - if (activeViewport === OrthoViews.TDView) { - return null; - } - - const SceneController = getSceneController(); - const { userBoundingBoxHitPlanesGroup } = SceneController; - const intersection = this.performHitTestForSceneGroup( - storeState, - userBoundingBoxHitPlanesGroup, - mousePosition, - activeViewport, - true, - ); - const hitObject = intersection != null ? intersection.object : null; - // Check whether we are hitting the same object as before, since we can return early - // in this case. - const didHitSamePlane = - hitObject != null && - this.lastBoundingBoxHit != null && - this.lastBoundingBoxHit.userData.plane === hitObject.userData.plane; - - if (hitObject === this.lastBoundingBoxHit || didHitSamePlane) { - if (hitObject != null) { - console.log("Hit the same object"); - } - this.lastBoundingBoxHit = hitObject; - return hitObject; - } - - // Undo highlighting of old hit - if (this.lastBoundingBoxHit != null) { - // Get HSL, save in userData, light the hsl up and set the new color. - // changing emissive doesnt work for this material. - const { userData } = this.lastBoundingBoxHit; - const previousHighlightedCrossection = userData.cube.crossSections[userData.plane]; - previousHighlightedCrossection.material.color.r = 0; - previousHighlightedCrossection.material.color.g = 1; - previousHighlightedCrossection.material.color.b = 0; - } - - this.lastBoundingBoxHit = hitObject; - - // Highlight new hit - if (hitObject != null) { - // TODO: gucken warum sich die Farbe der BBoxen nicht ändert. Scheint das falsche intersection object zu sein!!!! - // const lightenedColor = { h: hslColor.h, s: 1, l: 1 }; - const { userData } = hitObject; - const newHighlightedCrosssection = userData.cube.crossSections[userData.plane]; - newHighlightedCrosssection.material.color.r = 1; - newHighlightedCrosssection.material.color.g = 0; - newHighlightedCrosssection.material.color.b = 0; - } - this.draw(); - return hitObject; - } - draw(): void { app.vent.trigger("rerender"); } From fa0c76a0504b6c9bc5354f15f516205abe32fb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 14 Oct 2021 17:13:27 +0200 Subject: [PATCH 07/34] fix tests --- .../javascripts/test/reducers/volumetracing_reducer.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js index 30cef008481..21579bd2bd4 100644 --- a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js +++ b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js @@ -278,6 +278,9 @@ test("VolumeTracing should cycle trace/view/brush tool", t => { newState = UiReducer(newState, cycleToolAction); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.PICK_CELL); + newState = UiReducer(newState, cycleToolAction); + t.is(newState.uiInformation.activeTool, AnnotationToolEnum.BOUNDING_BOX); + // Cycle tool back to MOVE newState = UiReducer(newState, cycleToolAction); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.MOVE); From e3e8b94a5ccf2771a0e4a8baa9cfd2327876a0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 15 Oct 2021 15:54:34 +0200 Subject: [PATCH 08/34] add new bbox on left click, - reduce global position calculation complexity - no dashed lines as highlighed bbox anymore --- .../combinations/bounding_box_handlers.js | 27 +++++++- .../controller/combinations/tool_controls.js | 20 ++---- .../javascripts/oxalis/geometries/cube.js | 9 +-- .../model/accessors/view_mode_accessor.js | 61 ++++++++++++++++--- .../model/reducers/annotation_reducer.js | 2 +- .../oxalis/model/sagas/volumetracing_saga.js | 2 +- .../right-border-tabs/bounding_box_tab.js | 2 +- 7 files changed, 87 insertions(+), 36 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index 1c0efac3658..fa4b65853d2 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -1,9 +1,14 @@ // @flow -import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor"; +import { + calculateGlobalPos, + getDisplayedDataExtentInPlaneMode, +} from "oxalis/model/accessors/view_mode_accessor"; import { type OrthoView, type Point2, type Vector3 } from "oxalis/constants"; import Store from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import Dimension from "oxalis/model/dimensions"; +import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; +import * as Utils from "libs/utils"; /* const neighbourEdgeIndexByEdgeIndex = { // TODO: Use this to detect corners properly. @@ -68,7 +73,7 @@ function getDistanceToBoundingBoxEdge( return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]); } -export default function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView) { +export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView) { const state = Store.getState(); const globalPosition = calculateGlobalPos(state, pos, plane); const { userBoundingBoxes } = getSomeTracing(state.tracing); @@ -145,3 +150,21 @@ export default function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoVi resizableDimension, }; } + +export function createNewBoundingBoxAtCenter() { + // TODO: This behaviour is used multiple times in the code. Deduplicate this! + const state = Store.getState(); + const { min, max } = getDisplayedDataExtentInPlaneMode(state); + const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); + const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); + const boundingBoxId = highestBoundingBoxId + 1; + const newUserBoundingBox = { + boundingBox: { min, max }, + id: boundingBoxId, + name: `user bounding box ${boundingBoxId}`, + color: Utils.getRandomColor(), + isVisible: true, + }; + const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; + Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); +} diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 2f81c47f5d8..938bf7b30be 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -22,7 +22,10 @@ import * as MoveHandlers from "oxalis/controller/combinations/move_handlers"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import PlaneView from "oxalis/view/plane_view"; import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers"; -import getClosestHoveredBoundingBox from "oxalis/controller/combinations/bounding_box_handlers"; +import { + getClosestHoveredBoundingBox, + createNewBoundingBoxAtCenter, +} from "oxalis/controller/combinations/bounding_box_handlers"; import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; import Store from "oxalis/store"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; @@ -609,20 +612,7 @@ export class BoundingBoxTool { console.log("BoundingBox tool right up"); }, mouseMove: getClosestHoveredBoundingBoxThrottled, - leftClick: (_pos: Point2, _plane: OrthoView, _event: MouseEvent) => { - /* const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); - const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); - const boundingBoxId = highestBoundingBoxId + 1; - const newUserBoundingBox = { - boundingBox: { min: [100, 100, 100], max: [200, 200, 200] }, - id: boundingBoxId, - name: `user bounding box ${boundingBoxId}`, - color: Utils.getRandomColor(), - isVisible: true, - }; - const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; - Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); */ - }, + leftClick: createNewBoundingBoxAtCenter, rightClick: (_pos: Point2, _plane: OrthoView, _event: MouseEvent, _isTouch: boolean) => { console.log("BoundingBox right click"); diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index cc3b7d9fd33..99f328048ea 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -78,13 +78,7 @@ class Cube { getLineMaterial() { return this.isHighlighted - ? new THREE.LineDashedMaterial({ - color: 0xffffff, - linewidth: this.lineWidth, - scale: 1, - dashSize: 5, - gapSize: 5, - }) + ? new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: this.lineWidth }) : new THREE.LineBasicMaterial({ color: this.color, linewidth: this.lineWidth }); } @@ -143,7 +137,6 @@ class Cube { for (const mesh of _.values(this.crossSections).concat([this.cube])) { mesh.geometry.computeBoundingSphere(); - mesh.computeLineDistances(); mesh.geometry.verticesNeedUpdate = true; } diff --git a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js index 46af12f2aca..3a46fc9194f 100644 --- a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js @@ -15,8 +15,12 @@ import constants, { type Vector3, type ViewMode, } from "oxalis/constants"; +import { V3 } from "libs/mjs"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; -import { getPosition, getPlaneScalingFactor } from "oxalis/model/accessors/flycam_accessor"; +import { + getPosition, + getPlaneExtentInVoxelFromStore, +} from "oxalis/model/accessors/flycam_accessor"; import { reuseInstanceOnEquality } from "oxalis/model/accessors/accessor_helpers"; export function getTDViewportSize(state: OxalisState): [number, number] { @@ -102,14 +106,12 @@ function _calculateGlobalPos(state: OxalisState, clickPos: Point2, planeId: ?Ort let position; planeId = planeId || state.viewModeData.plane.activeViewport; const curGlobalPos = getPosition(state.flycam); - const zoomFactors = getPlaneScalingFactor(state, state.flycam, planeId); - const viewportScale = getViewportScale(state, planeId); const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); - - const center = [0, 1].map(dim => (constants.VIEWPORT_WIDTH * viewportScale[dim]) / 2); - const diffX = ((center[0] - clickPos.x) / viewportScale[0]) * zoomFactors[0]; - const diffY = ((center[1] - clickPos.y) / viewportScale[1]) * zoomFactors[1]; - + const { width, height } = getInputCatcherRect(state, planeId); + // Subtract clickPos from only half of the viewport extent as + // the center of the viewport / the flycam position is used as a reference point. + const diffX = (width / 2 - clickPos.x) * state.flycam.zoomStep; + const diffY = (height / 2 - clickPos.y) * state.flycam.zoomStep; switch (planeId) { case OrthoViews.PLANE_XY: position = [ @@ -142,6 +144,49 @@ function _calculateGlobalPos(state: OxalisState, clickPos: Point2, planeId: ?Ort return position; } +export function getDisplayedDataExtentInPlaneMode(state: OxalisState) { + const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); + const curGlobalCenterPos = getPosition(state.flycam); + const xyExtent = getPlaneExtentInVoxelFromStore( + state, + state.flycam.zoomStep, + OrthoViews.PLANE_XY, + ); + const yzExtent = getPlaneExtentInVoxelFromStore( + state, + state.flycam.zoomStep, + OrthoViews.PLANE_YZ, + ); + const xzExtent = getPlaneExtentInVoxelFromStore( + state, + state.flycam.zoomStep, + OrthoViews.PLANE_XZ, + ); + const minExtent = 1; + const getMinExtent = (val1, val2) => { + if (val1 <= 0) { + return Math.max(val2, minExtent); + } + if (val2 <= 0) { + return Math.max(val1, minExtent); + } + return val1 < val2 ? val1 : val2; + }; + const xMinExtent = getMinExtent(xyExtent[0], xzExtent[0]) * planeRatio[0]; + const yMinExtent = getMinExtent(xyExtent[1], yzExtent[1]) * planeRatio[1]; + const zMinExtent = getMinExtent(xzExtent[1], yzExtent[0]) * planeRatio[2]; + const extentFactor = 0.25; + const boxExtent = [ + Math.max(xMinExtent * extentFactor, 1), + Math.max(yMinExtent * extentFactor, 1), + Math.max(zMinExtent * extentFactor, 1), + ]; + return { + min: V3.toArray(V3.round(V3.sub(curGlobalCenterPos, boxExtent))), + max: V3.toArray(V3.round(V3.add(curGlobalCenterPos, boxExtent))), + }; +} + export const calculateGlobalPos = reuseInstanceOnEquality(_calculateGlobalPos); export function getViewMode(state: OxalisState): ViewMode { diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index e62fea0e448..a471db63efa 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -76,7 +76,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { if (tracing == null) { return state; } - let highestBoundingBoxId = Math.max(-1, ...tracing.userBoundingBoxes.map(bb => bb.id)); + let highestBoundingBoxId = Math.max(0, ...tracing.userBoundingBoxes.map(bb => bb.id)); const additionalUserBoundingBoxes = action.userBoundingBoxes.map(bb => { highestBoundingBoxId++; return { ...bb, id: highestBoundingBoxId }; diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index 0bdc5fc7373..82b5d86f61e 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -597,7 +597,7 @@ export function* floodFill(): Saga { const userBoundingBoxes = yield* select( state => getSomeTracing(state.tracing).userBoundingBoxes, ); - const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); + const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); const boundingBoxId = highestBoundingBoxId + 1; yield* put( diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js index 4fb79fdaef7..c48fec698ee 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js @@ -58,7 +58,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { function handleAddNewUserBoundingBox() { const datasetBoundingBox = getDatasetExtentInVoxel(dataset); // We use the default of -1 to get the id 0 for the first user bounding box. - const highestBoundingBoxId = Math.max(-1, ...userBoundingBoxes.map(bb => bb.id)); + const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); const boundingBoxId = highestBoundingBoxId + 1; const newUserBoundingBox = { boundingBox: Utils.computeBoundingBoxFromBoundingBoxObject(datasetBoundingBox), From 299ece47bbcb7fa9ba1bae2b4e9c3a34f39a1194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 15 Oct 2021 18:21:02 +0200 Subject: [PATCH 09/34] add snapping to resizing bounding boxes --- .../combinations/bounding_box_handlers.js | 70 +++++++++++++------ .../combinations/skeleton_handlers.js | 21 +----- .../controller/combinations/tool_controls.js | 60 +++++++--------- .../model/actions/annotation_actions.js | 13 ++++ .../model/reducers/annotation_reducer.js | 26 +++++++ .../oxalis/model/sagas/volumetracing_saga.js | 30 +++----- .../right-border-tabs/bounding_box_tab.js | 26 +++---- 7 files changed, 136 insertions(+), 110 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index fa4b65853d2..b5bcbcf8678 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -1,14 +1,11 @@ // @flow -import { - calculateGlobalPos, - getDisplayedDataExtentInPlaneMode, -} from "oxalis/model/accessors/view_mode_accessor"; +import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor"; +import _ from "lodash"; import { type OrthoView, type Point2, type Vector3 } from "oxalis/constants"; import Store from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import Dimension from "oxalis/model/dimensions"; import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; -import * as Utils from "libs/utils"; /* const neighbourEdgeIndexByEdgeIndex = { // TODO: Use this to detect corners properly. @@ -73,7 +70,16 @@ function getDistanceToBoundingBoxEdge( return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]); } -export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView) { +export type SelectedEdge = { + boxId: number, + dimensionIndex: 0 | 1 | 2, + direction: "horizontal" | "vertical", + isMaxEdge: boolean, + nearestEdgeIndex: number, + resizableDimension: 0 | 1 | 2, +}; + +export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?SelectedEdge { const state = Store.getState(); const globalPosition = calculateGlobalPos(state, pos, plane); const { userBoundingBoxes } = getSomeTracing(state.tracing); @@ -151,20 +157,44 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView) { }; } -export function createNewBoundingBoxAtCenter() { - // TODO: This behaviour is used multiple times in the code. Deduplicate this! +export function handleMovingBoundingBox( + mousePosition: Point2, + planeId: OrthoView, + selectedEdge: SelectedEdge, +) { const state = Store.getState(); - const { min, max } = getDisplayedDataExtentInPlaneMode(state); - const { userBoundingBoxes } = getSomeTracing(Store.getState().tracing); - const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); - const boundingBoxId = highestBoundingBoxId + 1; - const newUserBoundingBox = { - boundingBox: { min, max }, - id: boundingBoxId, - name: `user bounding box ${boundingBoxId}`, - color: Utils.getRandomColor(), - isVisible: true, - }; - const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; + const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId); + const { userBoundingBoxes } = getSomeTracing(state.tracing); + let didMinAndMaxSwitch = false; + const updatedUserBoundingBoxes = userBoundingBoxes.map(bbox => { + if (bbox.id !== selectedEdge.boxId) { + return bbox; + } + bbox = _.cloneDeep(bbox); + const { resizableDimension } = selectedEdge; + // For a horizontal edge only consider delta.y, for vertical only delta.x + const newPositionValue = Math.round(globalMousePosition[resizableDimension]); + const minOrMax = selectedEdge.isMaxEdge ? "max" : "min"; + const oppositeOfMinOrMax = selectedEdge.isMaxEdge ? "min" : "max"; + const otherEdgeValue = bbox.boundingBox[oppositeOfMinOrMax][resizableDimension]; + if (otherEdgeValue === newPositionValue) { + // Do not allow the same value for min and max for one dimension. + return bbox; + } + const areMinAndMaxEdgeCrossing = + // If the min / max edge is moved over the other one. + (selectedEdge.isMaxEdge && newPositionValue < otherEdgeValue) || + (!selectedEdge.isMaxEdge && newPositionValue > otherEdgeValue); + if (areMinAndMaxEdgeCrossing) { + // As the edge moved over the other one, the values for min and max must be switched. + bbox.boundingBox[minOrMax][resizableDimension] = otherEdgeValue; + bbox.boundingBox[oppositeOfMinOrMax][resizableDimension] = newPositionValue; + didMinAndMaxSwitch = true; + } else { + bbox.boundingBox[minOrMax][resizableDimension] = newPositionValue; + } + return bbox; + }); Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); + return { didMinAndMaxSwitch }; } diff --git a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js index 619ec518ae5..ca9c2d94472 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js @@ -46,6 +46,7 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import { renderToTexture } from "oxalis/view/rendering_utils"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import Dimensions from "oxalis/model/dimensions"; +import { getClosestHoveredBoundingBox } from "oxalis/controller/combinations/bounding_box_handlers"; const OrthoViewToNumber: OrthoViewMap = { [OrthoViews.PLANE_XY]: 0, @@ -138,6 +139,7 @@ export function handleOpenContextMenu( } const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); const state = Store.getState(); + const hoveredEdge = getClosestHoveredBoundingBox(position, plane); const globalPosition = calculateGlobalPos(state, position); showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, activeViewport); } @@ -168,25 +170,6 @@ export function moveNode(dx: number, dy: number, nodeId: ?number) { ); } -export function openContextMenu( - planeView: PlaneView, - position: Point2, - plane: OrthoView, - isTouch: boolean, - event: MouseEvent, - showNodeContextMenuAt: ShowContextMenuFunction, -) { - const state = Store.getState(); - const { activeViewport } = state.viewModeData.plane; - if (activeViewport === OrthoViews.TDView) { - return; - } - - const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); - const globalPosition = calculateGlobalPos(state, position); - showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, activeViewport); -} - export function setWaypoint( position: Vector3, activeViewport: OrthoView, diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 938bf7b30be..169d9e7134e 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -14,21 +14,20 @@ import { getContourTracingMode, enforceVolumeTracing, } from "oxalis/model/accessors/volumetracing_accessor"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { handleAgglomerateSkeletonAtClick } from "oxalis/controller/combinations/segmentation_handlers"; import { hideBrushAction } from "oxalis/model/actions/volumetracing_actions"; import { isBrushTool } from "oxalis/model/accessors/tool_accessor"; import getSceneController from "oxalis/controller/scene_controller_provider"; import * as MoveHandlers from "oxalis/controller/combinations/move_handlers"; -import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import PlaneView from "oxalis/view/plane_view"; import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers"; import { + type SelectedEdge, getClosestHoveredBoundingBox, - createNewBoundingBoxAtCenter, + handleMovingBoundingBox, } from "oxalis/controller/combinations/bounding_box_handlers"; -import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; import Store from "oxalis/store"; -import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import * as Utils from "libs/utils"; import * as VolumeHandlers from "oxalis/controller/combinations/volume_handlers"; import api from "oxalis/api/internal_api"; @@ -121,7 +120,7 @@ export class MoveTool { showNodeContextMenuAt: ShowContextMenuFunction, ) { return (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => - SkeletonHandlers.openContextMenu( + SkeletonHandlers.handleOpenContextMenu( planeView, pos, plane, @@ -388,7 +387,7 @@ export class DrawTool { return; } - SkeletonHandlers.openContextMenu( + SkeletonHandlers.handleOpenContextMenu( planeView, pos, plane, @@ -445,7 +444,7 @@ export class EraseTool { }, rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { - SkeletonHandlers.openContextMenu( + SkeletonHandlers.handleOpenContextMenu( planeView, pos, plane, @@ -533,11 +532,11 @@ export class FillCellTool { export class BoundingBoxTool { static getPlaneMouseControls( planeId: OrthoView, - _planeView: PlaneView, - _showNodeContextMenuAt: ShowContextMenuFunction, + planeView: PlaneView, + showNodeContextMenuAt: ShowContextMenuFunction, ): * { const bboxHoveringThrottleTime = 75; - let selectedEdge = null; + let selectedEdge: ?SelectedEdge = null; const getClosestHoveredBoundingBoxThrottled = planeId !== OrthoViews.TDView ? _.throttle((delta: Point2, position: Point2, _id, _event) => { @@ -560,30 +559,12 @@ export class BoundingBoxTool { }, bboxHoveringThrottleTime) : () => {}; return { - leftDownMove: (delta: Point2, _pos: Point2, _id: ?string, _event: MouseEvent) => { + leftDownMove: (delta: Point2, pos: Point2, _id: ?string, _event: MouseEvent) => { if (selectedEdge != null) { - const state = Store.getState(); - const currentlySelectedEdge = selectedEdge; // avoiding flow complains. - const { userBoundingBoxes } = getSomeTracing(state.tracing); - const zoomFactor = state.flycam.zoomStep; - const scaleFactor = getBaseVoxelFactors(state.dataset.dataSource.scale); - const updatedUserBoundingBoxes = userBoundingBoxes.map(bbox => { - if (bbox.id !== currentlySelectedEdge.boxId) { - return bbox; - } - bbox = _.cloneDeep(bbox); - const { resizableDimension } = currentlySelectedEdge; - // For a horizontal edge only consider delta.y, for vertical only delta.x - const movement = currentlySelectedEdge.direction === "horizontal" ? delta.y : delta.x; - const minOrMax = currentlySelectedEdge.isMaxEdge ? "max" : "min"; - const scaledMovement = movement * zoomFactor * scaleFactor[resizableDimension]; - bbox.boundingBox[minOrMax][resizableDimension] = Math.round( - bbox.boundingBox[minOrMax][resizableDimension] + scaledMovement, - ); - return bbox; - }); - - Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); + const { didMinAndMaxSwitch } = handleMovingBoundingBox(pos, planeId, selectedEdge); + if (didMinAndMaxSwitch) { + selectedEdge.isMaxEdge = !selectedEdge.isMaxEdge; + } } else { MoveHandlers.handleMovePlane(delta); } @@ -612,10 +593,17 @@ export class BoundingBoxTool { console.log("BoundingBox tool right up"); }, mouseMove: getClosestHoveredBoundingBoxThrottled, - leftClick: createNewBoundingBoxAtCenter, + leftClick: () => Store.dispatch(addUserBoundingBoxAction()), - rightClick: (_pos: Point2, _plane: OrthoView, _event: MouseEvent, _isTouch: boolean) => { - console.log("BoundingBox right click"); + rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { + SkeletonHandlers.handleOpenContextMenu( + planeView, + pos, + plane, + isTouch, + event, + showNodeContextMenuAt, + ); }, }; } diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index a7c731d5606..c6181d6a1e2 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -44,6 +44,11 @@ type AddUserBoundingBoxesAction = { userBoundingBoxes: Array, }; +type AddNewUserBoundingBox = { + type: "ADD_NEW_USER_BOUNDING_BOX", + newBoundingBox?: UserBoundingBox, +}; + export type UpdateRemoteMeshMetaDataAction = { type: "UPDATE_REMOTE_MESH_METADATA", id: string, @@ -149,6 +154,7 @@ export type AnnotationActionTypes = | SetAnnotationAllowUpdateAction | UpdateRemoteMeshMetaDataAction | SetUserBoundingBoxesAction + | AddNewUserBoundingBox | AddUserBoundingBoxesAction | AddMeshMetadataAction | DeleteMeshAction @@ -210,6 +216,13 @@ export const setUserBoundingBoxesAction = ( userBoundingBoxes, }); +export const addUserBoundingBoxAction = ( + newBoundingBox?: UserBoundingBox, +): AddNewUserBoundingBox => ({ + type: "ADD_NEW_USER_BOUNDING_BOX", + newBoundingBox, +}); + export const addUserBoundingBoxesAction = ( userBoundingBoxes: Array, ): AddUserBoundingBoxesAction => ({ diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index a471db63efa..625765edf4a 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -10,6 +10,8 @@ import { updateKey2, updateKey3, } from "oxalis/model/helpers/deep_update"; +import * as Utils from "libs/utils"; +import { getDisplayedDataExtentInPlaneMode } from "oxalis/model/accessors/view_mode_accessor"; import { convertServerAnnotationToFrontendAnnotation } from "oxalis/model/reducers/reducer_helpers"; const updateTracing = (state: OxalisState, shape: StateShape1<"tracing">): OxalisState => @@ -71,6 +73,30 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, action.userBoundingBoxes); } + case "ADD_NEW_USER_BOUNDING_BOX": { + const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + if (tracing == null) { + return state; + } + const { userBoundingBoxes } = tracing; + const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); + const boundingBoxId = highestBoundingBoxId + 1; + if (action.newBoundingBox != null) { + action.newBoundingBox.id = boundingBoxId; + } else { + const { min, max } = getDisplayedDataExtentInPlaneMode(state); + action.newBoundingBox = { + boundingBox: { min, max }, + id: boundingBoxId, + name: `user bounding box ${boundingBoxId}`, + color: Utils.getRandomColor(), + isVisible: true, + }; + } + const updatedUserBoundingBoxes = [...userBoundingBoxes, action.newBoundingBox]; + return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + } + case "ADD_USER_BOUNDING_BOXES": { const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; if (tracing == null) { diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index 82b5d86f61e..bdcdeaff9d4 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -9,8 +9,7 @@ import { updateDirectionAction, finishAnnotationStrokeAction, } from "oxalis/model/actions/volumetracing_actions"; -import { addUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; -import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { type Saga, _takeEvery, @@ -594,24 +593,17 @@ export function* floodFill(): Saga { }, ); - const userBoundingBoxes = yield* select( - state => getSomeTracing(state.tracing).userBoundingBoxes, - ); - const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); - const boundingBoxId = highestBoundingBoxId + 1; - yield* put( - addUserBoundingBoxesAction([ - { - id: boundingBoxId, - boundingBox: coveredBoundingBox, - name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( - ",", - )}, timestamp=${new Date().getTime()})`, - color: Utils.getRandomColor(), - isVisible: true, - }, - ]), + addUserBoundingBoxAction({ + // The id will be set by the action. + id: 0, + boundingBox: coveredBoundingBox, + name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( + ",", + )}, timestamp=${new Date().getTime()})`, + color: Utils.getRandomColor(), + isVisible: true, + }), ); } else { yield* call(progressCallback, true, "Floodfill done."); diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js index c48fec698ee..28f103e29c9 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js @@ -16,8 +16,10 @@ import { } from "oxalis/view/components/setting_input_views"; import type { OxalisState, Tracing, UserBoundingBox } from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; -import { getDatasetExtentInVoxel } from "oxalis/model/accessors/dataset_accessor"; -import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; +import { + setUserBoundingBoxesAction, + addUserBoundingBoxAction, +} from "oxalis/model/actions/annotation_actions"; import * as Utils from "libs/utils"; import ExportBoundingBoxModal from "oxalis/view/right-border-tabs/export_bounding_box_modal"; @@ -25,12 +27,13 @@ import ExportBoundingBoxModal from "oxalis/view/right-border-tabs/export_boundin type BoundingBoxTabProps = { tracing: Tracing, onChangeBoundingBoxes: (value: Array) => void, + addNewBoundingBox: () => void, dataset: APIDataset, }; function BoundingBoxTab(props: BoundingBoxTabProps) { const [selectedBoundingBoxForExport, setSelectedBoundingBoxForExport] = useState(null); - const { tracing, dataset, onChangeBoundingBoxes } = props; + const { tracing, dataset, onChangeBoundingBoxes, addNewBoundingBox } = props; const { userBoundingBoxes } = getSomeTracing(tracing); function handleChangeUserBoundingBox( @@ -56,19 +59,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { } function handleAddNewUserBoundingBox() { - const datasetBoundingBox = getDatasetExtentInVoxel(dataset); - // We use the default of -1 to get the id 0 for the first user bounding box. - const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); - const boundingBoxId = highestBoundingBoxId + 1; - const newUserBoundingBox = { - boundingBox: Utils.computeBoundingBoxFromBoundingBoxObject(datasetBoundingBox), - id: boundingBoxId, - name: `user bounding box ${boundingBoxId}`, - color: Utils.getRandomColor(), - isVisible: true, - }; - const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; - onChangeBoundingBoxes(updatedUserBoundingBoxes); + addNewBoundingBox(); } function handleDeleteUserBoundingBox(id: number) { @@ -130,6 +121,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ onChangeBoundingBoxes(userBoundingBoxes: Array) { dispatch(setUserBoundingBoxesAction(userBoundingBoxes)); }, + addNewBoundingBox() { + dispatch(addUserBoundingBoxAction()); + }, }); export default connect( From 51f2d316658e899f5cc7b5a68d08e0958b9ae0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 18 Oct 2021 11:28:41 +0200 Subject: [PATCH 10/34] add new bbox button to toolbar and context menu entry --- .../oxalis/view/action-bar/toolbar_view.js | 49 +++++++++++-- .../javascripts/oxalis/view/context_menu.js | 32 +++++++-- public/images/bounding-box.svg | 54 ++++++++++++++ public/images/new-bounding-box.svg | 70 +++++++++++++++++++ 4 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 public/images/bounding-box.svg create mode 100644 public/images/new-bounding-box.svg diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js index 619931872cc..f4c4f93ccae 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js @@ -33,6 +33,7 @@ import { createTreeAction, setMergerModeEnabledAction, } from "oxalis/model/actions/skeletontracing_actions"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { getActiveTree } from "oxalis/model/accessors/skeletontracing_accessor"; import { LogSliderSetting } from "oxalis/view/components/setting_input_views"; import { userSettings } from "types/schemas/user_settings.schema"; @@ -44,8 +45,8 @@ const narrowButtonStyle = { }; const imgStyleForSpaceyIcons = { - width: 14, - height: 14, + width: 19, + height: 19, lineHeight: 10, marginTop: -2, }; @@ -98,6 +99,10 @@ const handleCreateCell = () => { Store.dispatch(createCellAction()); }; +const handleAddNewUserBoundingBox = () => { + Store.dispatch(addUserBoundingBoxAction()); +}; + const handleSetOverwriteMode = (event: { target: { value: OverwriteMode } }) => { Store.dispatch(updateUserSettingAction("overwriteMode", event.target.value)); }; @@ -250,6 +255,23 @@ function CreateCellButton() { ); } +function CreateNewBoundingBoxButton() { + return ( + + + New Bounding Box Icon + + + ); +} + function CreateTreeButton() { const dispatch = useDispatch(); const activeTree = useSelector(state => toNullable(getActiveTree(state.tracing.skeleton))); @@ -561,7 +583,16 @@ export default function ToolbarView() { style={narrowButtonStyle} value={AnnotationToolEnum.BOUNDING_BOX} > - BBox + Trace Tool Icon ) : null} @@ -586,8 +617,12 @@ function ToolSpecificSettings({ isShiftPressed, }) { const showCreateTreeButton = hasSkeleton && adaptedActiveTool === AnnotationToolEnum.SKELETON; + const showNewBoundingBoxButton = adaptedActiveTool === AnnotationToolEnum.BOUNDING_BOX; const showCreateCellButton = - isVolumeSupported && !showCreateTreeButton && adaptedActiveTool !== AnnotationToolEnum.MOVE; + isVolumeSupported && + !showCreateTreeButton && + !showNewBoundingBoxButton && + adaptedActiveTool !== AnnotationToolEnum.MOVE; const showChangeBrushSizeButton = showCreateCellButton && (adaptedActiveTool === AnnotationToolEnum.BRUSH || @@ -602,6 +637,12 @@ function ToolSpecificSettings({ ) : null} + {showNewBoundingBoxButton ? ( + + + + ) : null} + {showCreateCellButton || showChangeBrushSizeButton ? ( {showCreateCellButton ? : null} diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index 7063de276e9..a85b6451913 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -15,6 +15,7 @@ import type { APIDataset, APIDataLayer } from "types/api_flow_types"; import type { Dispatch } from "redux"; import { connect } from "react-redux"; import { V3 } from "libs/mjs"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { deleteEdgeAction, mergeTreesAction, @@ -63,6 +64,7 @@ type DispatchProps = {| hideTree: number => void, createTree: () => void, setActiveCell: number => void, + addNewBoundingBox: () => void, |}; type StateProps = {| @@ -299,6 +301,7 @@ function NoNodeContextMenuOptions({ dataset, currentMeshFile, setActiveCell, + addNewBoundingBox, }: NoNodeContextMenuProps) { useEffect(() => { (async () => { @@ -387,14 +390,32 @@ function NoNodeContextMenuOptions({ , ] : []; + + const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; + const boundingBoxActions = [ + { + addNewBoundingBox(); + }} + > + Create new Bounding Box + {isBoundingBoxToolActive ? shortcutBuilder(["leftMouse"]) : null} + , + ]; if (volumeTracing == null && visibleSegmentationLayer != null) { nonSkeletonActions.push(loadMeshItem); } const isSkeletonToolActive = activeTool === AnnotationToolEnum.SKELETON; - - const allActions = isSkeletonToolActive - ? skeletonActions.concat(nonSkeletonActions) - : nonSkeletonActions.concat(skeletonActions); + let allActions = []; + if (isSkeletonToolActive) { + allActions = skeletonActions.concat(nonSkeletonActions).concat(boundingBoxActions); + } else if (isBoundingBoxToolActive) { + allActions = boundingBoxActions.concat(nonSkeletonActions).concat(skeletonActions); + } else { + allActions = nonSkeletonActions.concat(skeletonActions).concat(boundingBoxActions); + } if (allActions.length === 0) { return null; @@ -561,6 +582,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ setActiveCell(segmentId: number) { dispatch(setActiveCellAction(segmentId)); }, + addNewBoundingBox() { + dispatch(addUserBoundingBoxAction()); + }, }); function mapStateToProps(state: OxalisState): StateProps { diff --git a/public/images/bounding-box.svg b/public/images/bounding-box.svg new file mode 100644 index 00000000000..191a863c317 --- /dev/null +++ b/public/images/bounding-box.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/public/images/new-bounding-box.svg b/public/images/new-bounding-box.svg new file mode 100644 index 00000000000..28329a10290 --- /dev/null +++ b/public/images/new-bounding-box.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + From b30d229e94a42c9f453f734932be7ab84a1676df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 18 Oct 2021 11:36:12 +0200 Subject: [PATCH 11/34] reenable cursor chaning on hovered bbox --- .../oxalis/controller/combinations/tool_controls.js | 4 ++-- frontend/javascripts/oxalis/view/input_catcher.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 169d9e7134e..bf9417752d6 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -535,7 +535,7 @@ export class BoundingBoxTool { planeView: PlaneView, showNodeContextMenuAt: ShowContextMenuFunction, ): * { - const bboxHoveringThrottleTime = 75; + const bboxHoveringThrottleTime = 100; let selectedEdge: ?SelectedEdge = null; const getClosestHoveredBoundingBoxThrottled = planeId !== OrthoViews.TDView @@ -554,7 +554,7 @@ export class BoundingBoxTool { } } else { getSceneController().highlightUserBoundingBox(null); - body.style.cursor = "default"; + body.style.cursor = "auto"; } }, bboxHoveringThrottleTime) : () => {}; diff --git a/frontend/javascripts/oxalis/view/input_catcher.js b/frontend/javascripts/oxalis/view/input_catcher.js index a1c4083a0ae..36657deac8e 100644 --- a/frontend/javascripts/oxalis/view/input_catcher.js +++ b/frontend/javascripts/oxalis/view/input_catcher.js @@ -109,7 +109,7 @@ class InputCatcher extends React.PureComponent { className={`inputcatcher ${viewportID}`} style={{ position: "relative", - cursor: this.props.busyBlockingInfo.isBusy ? "wait" : "auto", + cursor: this.props.busyBlockingInfo.isBusy ? "wait" : "inherit", }} > From 852335535001668d1569fe6a60ac89880f708a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 18 Oct 2021 13:00:29 +0200 Subject: [PATCH 12/34] fix distored bounding box hit box --- .../combinations/bounding_box_handlers.js | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index b5bcbcf8678..d413d7fdd6d 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -6,6 +6,7 @@ import Store from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import Dimension from "oxalis/model/dimensions"; import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; +import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; /* const neighbourEdgeIndexByEdgeIndex = { // TODO: Use this to detect corners properly. @@ -31,6 +32,7 @@ function getDistanceToBoundingBoxEdge( compareToMin: boolean, edgeDim: number, otherDim: number, + planeRatio: Vector3, ) { // There are four cases how the distance to an edge needs to be calculated. // Here are all cases visualized via a number that are referenced below: @@ -49,25 +51,34 @@ function getDistanceToBoundingBoxEdge( // 2 2 // // This example is for the xy viewport for x as the main direction / edgeDim. + + // As the planeRatio is multiplied to the global coordinates passed to this method, + // the distance between the mouse and the bounding box is distorted by the factor of planeRatio. + // That's why we later divide exactly by this factor to let the hit box / distance + // between the mouse and bounding box be the same in each dimension. const cornerToCompareWith = compareToMin ? min : max; if (pos[edgeDim] < min[edgeDim]) { // Case 1: Distance to the min corner is needed in edgeDim. - return Math.sqrt( - Math.abs(pos[edgeDim] - min[edgeDim]) ** 2 + - Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, + return ( + Math.sqrt( + Math.abs(pos[edgeDim] - min[edgeDim]) ** 2 + + Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, + ) / planeRatio[edgeDim] ); } if (pos[edgeDim] > max[edgeDim]) { // Case 2: Distance to max Corner is needed in edgeDim. - return Math.sqrt( - Math.abs(pos[edgeDim] - max[edgeDim]) ** 2 + - Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, + return ( + Math.sqrt( + Math.abs(pos[edgeDim] - max[edgeDim]) ** 2 + + Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, + ) / planeRatio[edgeDim] ); } // Case 3: // If the position is within the bounds of the edgeDim, the shortest distance // to the edge is simply the difference between the otherDim values. - return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]); + return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) / planeRatio[edgeDim]; } export type SelectedEdge = { @@ -84,6 +95,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se const globalPosition = calculateGlobalPos(state, pos, plane); const { userBoundingBoxes } = getSomeTracing(state.tracing); const reorderedIndices = Dimension.getIndices(plane); + const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); const thirdDim = reorderedIndices[2]; let currentNearestDistance = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep; @@ -97,6 +109,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se if (!isCrossSectionOfViewportVisible) { continue; } + const distanceArray = [ getDistanceToBoundingBoxEdge( globalPosition, @@ -105,6 +118,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se true, reorderedIndices[0], reorderedIndices[1], + planeRatio, ), getDistanceToBoundingBoxEdge( globalPosition, @@ -113,6 +127,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se false, reorderedIndices[0], reorderedIndices[1], + planeRatio, ), getDistanceToBoundingBoxEdge( globalPosition, @@ -121,6 +136,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se true, reorderedIndices[1], reorderedIndices[0], + planeRatio, ), getDistanceToBoundingBoxEdge( globalPosition, @@ -129,6 +145,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se false, reorderedIndices[1], reorderedIndices[0], + planeRatio, ), ]; const minimumDistance = Math.min(...distanceArray); @@ -143,6 +160,7 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se } const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance); const dimensionOfNearestEdge = nearestEdgeIndex < 2 ? reorderedIndices[0] : reorderedIndices[1]; + console.log(currentNearestDistanceArray, planeRatio[dimensionOfNearestEdge]); const direction = nearestEdgeIndex < 2 ? "horizontal" : "vertical"; const isMaxEdge = nearestEdgeIndex % 2 === 1; const resizableDimension = nearestEdgeIndex < 2 ? reorderedIndices[1] : reorderedIndices[0]; From b0dfada2aaac011ebec73e3c183317b613ebbbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 18 Oct 2021 13:04:15 +0200 Subject: [PATCH 13/34] fix linting --- .../oxalis/controller/combinations/skeleton_handlers.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js index ca9c2d94472..cd51636ca4a 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js @@ -46,7 +46,6 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import { renderToTexture } from "oxalis/view/rendering_utils"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import Dimensions from "oxalis/model/dimensions"; -import { getClosestHoveredBoundingBox } from "oxalis/controller/combinations/bounding_box_handlers"; const OrthoViewToNumber: OrthoViewMap = { [OrthoViews.PLANE_XY]: 0, @@ -139,7 +138,6 @@ export function handleOpenContextMenu( } const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); const state = Store.getState(); - const hoveredEdge = getClosestHoveredBoundingBox(position, plane); const globalPosition = calculateGlobalPos(state, position); showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, activeViewport); } From 690fe84514430d6aecba09681a5b4315ee6373f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Wed, 20 Oct 2021 13:28:23 +0200 Subject: [PATCH 14/34] various changes - add move plane when alt is pressed - add c as shortcut to create a new bbox - remove left click to create new bbox - set center of bbox created with context menu at clicked position - remove `user` from default naming schema of bboxes --- .../combinations/bounding_box_handlers.js | 1 - .../controller/combinations/move_handlers.js | 9 +++ .../controller/combinations/tool_controls.js | 29 ++------- .../controller/viewmodes/plane_controller.js | 63 ++++++++++++++----- .../model/accessors/view_mode_accessor.js | 7 ++- .../model/actions/annotation_actions.js | 7 ++- .../model/reducers/annotation_reducer.js | 30 +++++++-- .../oxalis/model/reducers/reducer_helpers.js | 2 +- .../javascripts/oxalis/view/context_menu.js | 8 +-- .../javascripts/oxalis/view/version_entry.js | 2 +- 10 files changed, 100 insertions(+), 58 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index d413d7fdd6d..604910f350e 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -160,7 +160,6 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se } const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance); const dimensionOfNearestEdge = nearestEdgeIndex < 2 ? reorderedIndices[0] : reorderedIndices[1]; - console.log(currentNearestDistanceArray, planeRatio[dimensionOfNearestEdge]); const direction = nearestEdgeIndex < 2 ? "horizontal" : "vertical"; const isMaxEdge = nearestEdgeIndex % 2 === 1; const resizableDimension = nearestEdgeIndex < 2 ? reorderedIndices[1] : reorderedIndices[0]; diff --git a/frontend/javascripts/oxalis/controller/combinations/move_handlers.js b/frontend/javascripts/oxalis/controller/combinations/move_handlers.js index a2acf9e8ccc..7304fc5b010 100644 --- a/frontend/javascripts/oxalis/controller/combinations/move_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/move_handlers.js @@ -68,6 +68,15 @@ export const moveZ = (z: number, oneSlide: boolean): void => { } }; +export function moveWhenAltIsPressed(delta: Point2, position: Point2, _id: any, event: MouseEvent) { + // Always set the correct mouse position. Otherwise, using alt + mouse move and + // alt + scroll won't result in the correct zoomToMouse behavior. + setMousePosition(position); + if (event.altKey && !event.shiftKey && !event.ctrlKey) { + handleMovePlane(delta); + } +} + export const zoom = (value: number, zoomToMouse: boolean) => { const { activeViewport } = Store.getState().viewModeData.plane; if (OrthoViewValuesWithoutTDView.includes(activeViewport)) { diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index bf9417752d6..99aeb862d67 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -14,7 +14,6 @@ import { getContourTracingMode, enforceVolumeTracing, } from "oxalis/model/accessors/volumetracing_accessor"; -import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { handleAgglomerateSkeletonAtClick } from "oxalis/controller/combinations/segmentation_handlers"; import { hideBrushAction } from "oxalis/model/actions/volumetracing_actions"; import { isBrushTool } from "oxalis/model/accessors/tool_accessor"; @@ -99,14 +98,7 @@ export class MoveTool { } }, pinch: delta => MoveHandlers.zoom(delta, true), - mouseMove: (delta: Point2, position: Point2, id, event) => { - // Always set the correct mouse position. Otherwise, using alt + mouse move and - // alt + scroll won't result in the correct zoomToMouse behavior. - MoveHandlers.setMousePosition(position); - if (event.altKey && !event.shiftKey && !event.ctrlKey) { - MoveHandlers.handleMovePlane(delta); - } - }, + mouseMove: MoveHandlers.moveWhenAltIsPressed, leftDownMove: (delta: Point2, _pos: Point2, _id: ?string, _event: MouseEvent) => { MoveHandlers.handleMovePlane(delta); }, @@ -539,7 +531,7 @@ export class BoundingBoxTool { let selectedEdge: ?SelectedEdge = null; const getClosestHoveredBoundingBoxThrottled = planeId !== OrthoViews.TDView - ? _.throttle((delta: Point2, position: Point2, _id, _event) => { + ? _.throttle((delta: Point2, position: Point2) => { const { body } = document; if (body == null || selectedEdge != null) { return; @@ -557,7 +549,7 @@ export class BoundingBoxTool { body.style.cursor = "auto"; } }, bboxHoveringThrottleTime) - : () => {}; + : (_delta: Point2, _position: Point2) => {}; return { leftDownMove: (delta: Point2, pos: Point2, _id: ?string, _event: MouseEvent) => { if (selectedEdge != null) { @@ -581,19 +573,10 @@ export class BoundingBoxTool { getSceneController().highlightUserBoundingBox(null); }, - rightDownMove: (_delta: Point2, _pos: Point2) => { - console.log("BoundingBox tool right down move"); - }, - - rightMouseDown: (_pos: Point2, _plane: OrthoView, _event: MouseEvent) => { - console.log("BoundingBox tool right down"); - }, - - rightMouseUp: () => { - console.log("BoundingBox tool right up"); + mouseMove: (delta: Point2, position: Point2, _id, event: MouseEvent) => { + MoveHandlers.moveWhenAltIsPressed(delta, position, _id, event); + getClosestHoveredBoundingBoxThrottled(delta, position); }, - mouseMove: getClosestHoveredBoundingBoxThrottled, - leftClick: () => Store.dispatch(addUserBoundingBoxAction()), rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { SkeletonHandlers.handleOpenContextMenu( diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js index 40ad2523f6e..90839232e34 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js @@ -16,6 +16,7 @@ import { toggleAllTreesAction, toggleInactiveTreesAction, } from "oxalis/model/actions/skeletontracing_actions"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { InputKeyboard, InputKeyboardNoLoop, InputMouse } from "libs/input"; import { document } from "libs/window"; import { getBaseVoxel } from "oxalis/model/scaleinfo"; @@ -140,6 +141,14 @@ class VolumeKeybindings { } } +class BoundingBoxKeybindings { + static getKeyboardControls() { + return { + c: () => Store.dispatch(addUserBoundingBoxAction()), + }; + } +} + class PlaneController extends React.PureComponent { // See comment in Controller class on general controller architecture. // @@ -382,6 +391,8 @@ class PlaneController extends React.PureComponent { ? VolumeKeybindings.getKeyboardControls() : emptyDefaultHandler; + const { c: boundingBoxCHandler } = BoundingBoxKeybindings.getKeyboardControls(); + ensureNonConflictingHandlers(skeletonControls, volumeControls); return { @@ -389,7 +400,11 @@ class PlaneController extends React.PureComponent { ...skeletonControls, // $FlowIssue[exponential-spread] See https://github.com/facebook/flow/issues/8299 ...volumeControls, - c: this.createToolDependentKeyboardHandler(skeletonCHandler, volumeCHandler), + c: this.createToolDependentKeyboardHandler( + skeletonCHandler, + volumeCHandler, + boundingBoxCHandler, + ), "1": this.createToolDependentKeyboardHandler(skeletonOneHandler, volumeOneHandler), }; } @@ -463,28 +478,42 @@ class PlaneController extends React.PureComponent { createToolDependentKeyboardHandler( skeletonHandler: ?Function, volumeHandler: ?Function, + boundingBoxHandler: ?Function, viewHandler?: ?Function, ): Function { return (...args) => { const tool = this.props.activeTool; - if (tool === AnnotationToolEnum.MOVE) { - if (viewHandler != null) { - viewHandler(...args); - } else if (skeletonHandler != null) { - skeletonHandler(...args); + switch (tool) { + case AnnotationToolEnum.MOVE: { + if (viewHandler != null) { + viewHandler(...args); + } else if (skeletonHandler != null) { + skeletonHandler(...args); + } + return; + } + case AnnotationToolEnum.SKELETON: { + if (skeletonHandler != null) { + skeletonHandler(...args); + } else if (viewHandler != null) { + viewHandler(...args); + } + return; } - } else if (tool === AnnotationToolEnum.SKELETON) { - if (skeletonHandler != null) { - skeletonHandler(...args); - } else if (viewHandler != null) { - viewHandler(...args); + case AnnotationToolEnum.BOUNDING_BOX: { + if (boundingBoxHandler != null) { + boundingBoxHandler(...args); + } else if (viewHandler != null) { + viewHandler(...args); + } + return; } - } else { - // eslint-disable-next-line no-lonely-if - if (volumeHandler != null) { - volumeHandler(...args); - } else if (viewHandler != null) { - viewHandler(...args); + default: { + if (volumeHandler != null) { + volumeHandler(...args); + } else if (viewHandler != null) { + viewHandler(...args); + } } } }; diff --git a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js index 3a46fc9194f..e0ed1b3ff4e 100644 --- a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js @@ -176,14 +176,15 @@ export function getDisplayedDataExtentInPlaneMode(state: OxalisState) { const yMinExtent = getMinExtent(xyExtent[1], yzExtent[1]) * planeRatio[1]; const zMinExtent = getMinExtent(xzExtent[1], yzExtent[0]) * planeRatio[2]; const extentFactor = 0.25; - const boxExtent = [ + const halfBoxExtent = [ Math.max(xMinExtent * extentFactor, 1), Math.max(yMinExtent * extentFactor, 1), Math.max(zMinExtent * extentFactor, 1), ]; return { - min: V3.toArray(V3.round(V3.sub(curGlobalCenterPos, boxExtent))), - max: V3.toArray(V3.round(V3.add(curGlobalCenterPos, boxExtent))), + min: V3.toArray(V3.round(V3.sub(curGlobalCenterPos, halfBoxExtent))), + max: V3.toArray(V3.round(V3.add(curGlobalCenterPos, halfBoxExtent))), + halfBoxExtent, }; } diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index c6181d6a1e2..074cae163b9 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -46,7 +46,8 @@ type AddUserBoundingBoxesAction = { type AddNewUserBoundingBox = { type: "ADD_NEW_USER_BOUNDING_BOX", - newBoundingBox?: UserBoundingBox, + newBoundingBox?: ?UserBoundingBox, + center?: Vector3, }; export type UpdateRemoteMeshMetaDataAction = { @@ -217,10 +218,12 @@ export const setUserBoundingBoxesAction = ( }); export const addUserBoundingBoxAction = ( - newBoundingBox?: UserBoundingBox, + newBoundingBox?: ?UserBoundingBox, + center?: Vector3, ): AddNewUserBoundingBox => ({ type: "ADD_NEW_USER_BOUNDING_BOX", newBoundingBox, + center, }); export const addUserBoundingBoxesAction = ( diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 625765edf4a..009db171014 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -12,6 +12,7 @@ import { } from "oxalis/model/helpers/deep_update"; import * as Utils from "libs/utils"; import { getDisplayedDataExtentInPlaneMode } from "oxalis/model/accessors/view_mode_accessor"; +import { map3 } from "libs/utils"; import { convertServerAnnotationToFrontendAnnotation } from "oxalis/model/reducers/reducer_helpers"; const updateTracing = (state: OxalisState, shape: StateShape1<"tracing">): OxalisState => @@ -81,19 +82,36 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { const { userBoundingBoxes } = tracing; const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); const boundingBoxId = highestBoundingBoxId + 1; - if (action.newBoundingBox != null) { - action.newBoundingBox.id = boundingBoxId; + let { newBoundingBox } = action; + if (newBoundingBox != null) { + newBoundingBox.id = boundingBoxId; } else { - const { min, max } = getDisplayedDataExtentInPlaneMode(state); - action.newBoundingBox = { + const { min, max, halfBoxExtent } = getDisplayedDataExtentInPlaneMode(state); + newBoundingBox = { boundingBox: { min, max }, id: boundingBoxId, - name: `user bounding box ${boundingBoxId}`, + name: `bounding box ${boundingBoxId}`, color: Utils.getRandomColor(), isVisible: true, }; + if (action.center != null) { + const roundedCenter = map3(val => Math.round(val), action.center); + const roundedHalfBoxExtent = map3(val => Math.round(val), halfBoxExtent); + newBoundingBox.boundingBox = { + min: [ + roundedCenter[0] - roundedHalfBoxExtent[0], + roundedCenter[1] - roundedHalfBoxExtent[1], + roundedCenter[2] - roundedHalfBoxExtent[2], + ], + max: [ + roundedCenter[0] + roundedHalfBoxExtent[0], + roundedCenter[1] + roundedHalfBoxExtent[1], + roundedCenter[2] + roundedHalfBoxExtent[2], + ], + }; + } } - const updatedUserBoundingBoxes = [...userBoundingBoxes, action.newBoundingBox]; + const updatedUserBoundingBoxes = [...userBoundingBoxes, newBoundingBox]; return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); } diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js index 54eb1e0c4c7..309fa5b8aa2 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -53,7 +53,7 @@ export function convertUserBoundingBoxesFromServerToFrontend( boundingBox: convertedBoundingBox, color: color ? Utils.colorObjectToRGBArray(color) : Utils.getRandomColor(), id, - name: name || `user bounding box ${id}`, + name: name || `bounding box ${id}`, isVisible: isVisible != null ? isVisible : true, }; }); diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index a85b6451913..a8e96fbf3eb 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -64,7 +64,7 @@ type DispatchProps = {| hideTree: number => void, createTree: () => void, setActiveCell: number => void, - addNewBoundingBox: () => void, + addNewBoundingBox: Vector3 => void, |}; type StateProps = {| @@ -397,7 +397,7 @@ function NoNodeContextMenuOptions({ className="node-context-menu-item" key="add-new-bounding-box" onClick={() => { - addNewBoundingBox(); + addNewBoundingBox(globalPosition); }} > Create new Bounding Box @@ -582,8 +582,8 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ setActiveCell(segmentId: number) { dispatch(setActiveCellAction(segmentId)); }, - addNewBoundingBox() { - dispatch(addUserBoundingBoxAction()); + addNewBoundingBox(center: Vector3) { + dispatch(addUserBoundingBoxAction(null, center)); }, }); diff --git a/frontend/javascripts/oxalis/view/version_entry.js b/frontend/javascripts/oxalis/view/version_entry.js index 7047624bb2d..53b17866165 100644 --- a/frontend/javascripts/oxalis/view/version_entry.js +++ b/frontend/javascripts/oxalis/view/version_entry.js @@ -49,7 +49,7 @@ const descriptionFns = { icon: , }), updateUserBoundingBoxes: (): Description => ({ - description: "Updated a user bounding box.", + description: "Updated a bounding box.", icon: , }), removeFallbackLayer: (): Description => ({ From c25fe6520a6935169808d080846dfbf8a2b6fa18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Wed, 20 Oct 2021 14:33:00 +0200 Subject: [PATCH 15/34] add jump to bbox button and auto saving new bounding boxes --- .../model/actions/skeletontracing_actions.js | 1 + .../model/actions/volumetracing_actions.js | 1 + .../view/components/setting_input_views.js | 32 +++++++++++++++---- .../right-border-tabs/bounding_box_tab.js | 23 ++++++++++++- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js index eb62535e045..588ac287a84 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js @@ -200,6 +200,7 @@ export const SkeletonTracingSaveRelevantActions = [ "DELETE_COMMENT", "SET_USER_BOUNDING_BOXES", "ADD_USER_BOUNDING_BOXES", + "ADD_NEW_USER_BOUNDING_BOX", "SET_TREE_GROUPS", "SET_TREE_GROUP", "SET_MERGER_MODE_ENABLED", diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index e24b63b120e..27ef0fcbea9 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -73,6 +73,7 @@ export const VolumeTracingSaveRelevantActions = [ "SET_ACTIVE_CELL", "SET_USER_BOUNDING_BOXES", "ADD_USER_BOUNDING_BOXES", + "ADD_NEW_USER_BOUNDING_BOX", "FINISH_ANNOTATION_STROKE", ]; diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.js b/frontend/javascripts/oxalis/view/components/setting_input_views.js index 9126103a26f..bd5a5408360 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.js @@ -1,6 +1,7 @@ // @flow import { Row, Col, Slider, InputNumber, Switch, Tooltip, Input, Select, Popover } from "antd"; -import { DeleteOutlined, DownloadOutlined, EditOutlined } from "@ant-design/icons"; +import { DeleteOutlined, DownloadOutlined, EditOutlined, ScanOutlined } from "@ant-design/icons"; + import * as React from "react"; import _ from "lodash"; @@ -303,6 +304,7 @@ type UserBoundingBoxInputProps = { onChange: UserBoundingBoxInputUpdate => void, onDelete: () => void, onExport: () => void, + onGoToBoundingBox: () => void, }; type State = { @@ -387,9 +389,22 @@ export class UserBoundingBoxInput extends React.PureComponent colorPart * 255): any): Vector3); - const iconStyle = { margin: "auto 0px auto 6px" }; + const iconStyle = { + marginTop: "auto", + marginRight: 0, + marginBottom: "auto", + marginLeft: 6, + }; const exportIconStyle = isExportEnabled ? iconStyle : { ...iconStyle, opacity: 0.5, cursor: "not-allowed" }; @@ -403,7 +418,6 @@ export class UserBoundingBoxInput extends React.PureComponent ) : null; - const nameColSpan = exportColumn == null ? 17 : 15; return ( @@ -415,7 +429,8 @@ export class UserBoundingBoxInput extends React.PureComponent - + + - + + + + + + ); diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js index 28f103e29c9..8d79bcaa08d 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js @@ -10,6 +10,8 @@ import React, { useState } from "react"; import _ from "lodash"; import type { APIDataset } from "types/api_flow_types"; +import { setPositionAction } from "oxalis/model/actions/flycam_actions"; +import { type Vector3 } from "oxalis/constants"; import { UserBoundingBoxInput, type UserBoundingBoxInputUpdate, @@ -28,12 +30,13 @@ type BoundingBoxTabProps = { tracing: Tracing, onChangeBoundingBoxes: (value: Array) => void, addNewBoundingBox: () => void, + setPosition: Vector3 => void, dataset: APIDataset, }; function BoundingBoxTab(props: BoundingBoxTabProps) { const [selectedBoundingBoxForExport, setSelectedBoundingBoxForExport] = useState(null); - const { tracing, dataset, onChangeBoundingBoxes, addNewBoundingBox } = props; + const { tracing, dataset, onChangeBoundingBoxes, addNewBoundingBox, setPosition } = props; const { userBoundingBoxes } = getSomeTracing(tracing); function handleChangeUserBoundingBox( @@ -58,6 +61,20 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { onChangeBoundingBoxes(updatedUserBoundingBoxes); } + function handleGoToBoundingBox(id: number) { + const boundingBoxEntry = userBoundingBoxes.find(bbox => bbox.id === id); + if (!boundingBoxEntry) { + return; + } + const { min, max } = boundingBoxEntry.boundingBox; + const center = [ + min[0] + (max[0] - min[0]) / 2, + min[1] + (max[1] - min[1]) / 2, + min[2] + (max[2] - min[2]) / 2, + ]; + setPosition(center); + } + function handleAddNewUserBoundingBox() { addNewBoundingBox(); } @@ -84,6 +101,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { onExport={ dataset.jobsEnabled ? _.partial(setSelectedBoundingBoxForExport, bb) : () => {} } + onGoToBoundingBox={_.partial(handleGoToBoundingBox, bb.id)} /> )) ) : ( @@ -124,6 +142,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ addNewBoundingBox() { dispatch(addUserBoundingBoxAction()); }, + setPosition(position: Vector3) { + dispatch(setPositionAction(position)); + }, }); export default connect( From 5b4ba2a6a23f76b891f3ec02267d1ab080276f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Wed, 20 Oct 2021 16:53:11 +0200 Subject: [PATCH 16/34] add corner dragging --- .../combinations/bounding_box_handlers.js | 117 +++++++++++------- .../controller/combinations/tool_controls.js | 47 ++++--- .../view/components/setting_input_views.js | 2 - .../javascripts/oxalis/view/context_menu.js | 2 +- 4 files changed, 107 insertions(+), 61 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index 604910f350e..f7d777d7526 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -8,7 +8,7 @@ import Dimension from "oxalis/model/dimensions"; import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; -/* const neighbourEdgeIndexByEdgeIndex = { +const getNeighbourEdgeIndexByEdgeIndex = { // TODO: Use this to detect corners properly. // The edges are indexed within the plane like this: // See the distanceArray calculation as a reference. @@ -22,7 +22,7 @@ import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; "1": [2, 3], "2": [0, 1], "3": [0, 1], -}; */ +}; const MAX_DISTANCE_TO_SELECTION = 15; function getDistanceToBoundingBoxEdge( @@ -83,14 +83,16 @@ function getDistanceToBoundingBoxEdge( export type SelectedEdge = { boxId: number, - dimensionIndex: 0 | 1 | 2, direction: "horizontal" | "vertical", isMaxEdge: boolean, - nearestEdgeIndex: number, + edgeId: number, resizableDimension: 0 | 1 | 2, }; -export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?SelectedEdge { +export function getClosestHoveredBoundingBox( + pos: Point2, + plane: OrthoView, +): [SelectedEdge, ?SelectedEdge] | null { const state = Store.getState(); const globalPosition = calculateGlobalPos(state, pos, plane); const { userBoundingBoxes } = getSomeTracing(state.tracing); @@ -98,7 +100,8 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); const thirdDim = reorderedIndices[2]; - let currentNearestDistance = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep; + const zoomedMaxDistanceToSelection = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep; + let currentNearestDistance = zoomedMaxDistanceToSelection; let currentNearestBoundingBox = null; let currentNearestDistanceArray = null; @@ -109,7 +112,8 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se if (!isCrossSectionOfViewportVisible) { continue; } - + // In getNeighbourEdgeIndexByEdgeIndex is a visualization + // of how the indices of the array map to the visible bbox edges. const distanceArray = [ getDistanceToBoundingBoxEdge( globalPosition, @@ -158,60 +162,85 @@ export function getClosestHoveredBoundingBox(pos: Point2, plane: OrthoView): ?Se if (currentNearestBoundingBox == null || currentNearestDistanceArray == null) { return null; } - const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance); - const dimensionOfNearestEdge = nearestEdgeIndex < 2 ? reorderedIndices[0] : reorderedIndices[1]; - const direction = nearestEdgeIndex < 2 ? "horizontal" : "vertical"; - const isMaxEdge = nearestEdgeIndex % 2 === 1; - const resizableDimension = nearestEdgeIndex < 2 ? reorderedIndices[1] : reorderedIndices[0]; - // TODO: Add feature to select corners. - return { - boxId: currentNearestBoundingBox.id, - dimensionIndex: dimensionOfNearestEdge, - direction, - isMaxEdge, - nearestEdgeIndex, - resizableDimension, + const nearestBoundingBox = currentNearestBoundingBox; + const getEdgeInfoFromId = (edgeId: number) => { + const direction = edgeId < 2 ? "horizontal" : "vertical"; + const isMaxEdge = edgeId % 2 === 1; + const resizableDimension = edgeId < 2 ? reorderedIndices[1] : reorderedIndices[0]; + return { + boxId: nearestBoundingBox.id, + direction, + isMaxEdge, + edgeId, + resizableDimension, + }; }; + const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance); + const primaryEdge = getEdgeInfoFromId(nearestEdgeIndex); + let secondaryEdge = null; + const [firstNeighbourId, secondNeighbourId] = getNeighbourEdgeIndexByEdgeIndex[nearestEdgeIndex]; + const firstNeighbourEdgeDistance = currentNearestDistanceArray[firstNeighbourId]; + const secondNeighbourEdgeDistance = currentNearestDistanceArray[secondNeighbourId]; + if ( + firstNeighbourEdgeDistance < secondNeighbourEdgeDistance && + firstNeighbourEdgeDistance < zoomedMaxDistanceToSelection + ) { + secondaryEdge = getEdgeInfoFromId(firstNeighbourId); + } else if ( + secondNeighbourEdgeDistance < firstNeighbourEdgeDistance && + secondNeighbourEdgeDistance < zoomedMaxDistanceToSelection + ) { + secondaryEdge = getEdgeInfoFromId(secondNeighbourId); + } + return [primaryEdge, secondaryEdge]; } export function handleMovingBoundingBox( mousePosition: Point2, planeId: OrthoView, - selectedEdge: SelectedEdge, + primaryEdge: SelectedEdge, + secondaryEdge: ?SelectedEdge, ) { const state = Store.getState(); const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId); const { userBoundingBoxes } = getSomeTracing(state.tracing); - let didMinAndMaxSwitch = false; + const didMinAndMaxSwitch = { primary: false, secondary: false }; const updatedUserBoundingBoxes = userBoundingBoxes.map(bbox => { - if (bbox.id !== selectedEdge.boxId) { + if (bbox.id !== primaryEdge.boxId) { return bbox; } bbox = _.cloneDeep(bbox); - const { resizableDimension } = selectedEdge; - // For a horizontal edge only consider delta.y, for vertical only delta.x - const newPositionValue = Math.round(globalMousePosition[resizableDimension]); - const minOrMax = selectedEdge.isMaxEdge ? "max" : "min"; - const oppositeOfMinOrMax = selectedEdge.isMaxEdge ? "min" : "max"; - const otherEdgeValue = bbox.boundingBox[oppositeOfMinOrMax][resizableDimension]; - if (otherEdgeValue === newPositionValue) { - // Do not allow the same value for min and max for one dimension. - return bbox; + function applyPositionToEdge(edge: SelectedEdge): boolean { + const { resizableDimension } = edge; + // For a horizontal edge only consider delta.y, for vertical only delta.x + const newPositionValue = Math.round(globalMousePosition[resizableDimension]); + const minOrMax = edge.isMaxEdge ? "max" : "min"; + const oppositeOfMinOrMax = edge.isMaxEdge ? "min" : "max"; + const otherEdgeValue = bbox.boundingBox[oppositeOfMinOrMax][resizableDimension]; + if (otherEdgeValue === newPositionValue) { + // Do not allow the same value for min and max for one dimension. + return false; + } + const areMinAndMaxEdgeCrossing = + // If the min / max edge is moved over the other one. + (edge.isMaxEdge && newPositionValue < otherEdgeValue) || + (!edge.isMaxEdge && newPositionValue > otherEdgeValue); + if (areMinAndMaxEdgeCrossing) { + // As the edge moved over the other one, the values for min and max must be switched. + bbox.boundingBox[minOrMax][resizableDimension] = otherEdgeValue; + bbox.boundingBox[oppositeOfMinOrMax][resizableDimension] = newPositionValue; + return true; + } else { + bbox.boundingBox[minOrMax][resizableDimension] = newPositionValue; + return false; + } } - const areMinAndMaxEdgeCrossing = - // If the min / max edge is moved over the other one. - (selectedEdge.isMaxEdge && newPositionValue < otherEdgeValue) || - (!selectedEdge.isMaxEdge && newPositionValue > otherEdgeValue); - if (areMinAndMaxEdgeCrossing) { - // As the edge moved over the other one, the values for min and max must be switched. - bbox.boundingBox[minOrMax][resizableDimension] = otherEdgeValue; - bbox.boundingBox[oppositeOfMinOrMax][resizableDimension] = newPositionValue; - didMinAndMaxSwitch = true; - } else { - bbox.boundingBox[minOrMax][resizableDimension] = newPositionValue; + didMinAndMaxSwitch.primary = applyPositionToEdge(primaryEdge); + if (secondaryEdge) { + didMinAndMaxSwitch.secondary = applyPositionToEdge(secondaryEdge); } return bbox; }); Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); - return { didMinAndMaxSwitch }; + return didMinAndMaxSwitch; } diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 99aeb862d67..b0083a9bfa4 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -528,18 +528,27 @@ export class BoundingBoxTool { showNodeContextMenuAt: ShowContextMenuFunction, ): * { const bboxHoveringThrottleTime = 100; - let selectedEdge: ?SelectedEdge = null; + let primarySelectedEdge: ?SelectedEdge = null; + let secondarySelectedEdge: ?SelectedEdge = null; const getClosestHoveredBoundingBoxThrottled = planeId !== OrthoViews.TDView ? _.throttle((delta: Point2, position: Point2) => { const { body } = document; - if (body == null || selectedEdge != null) { + if (body == null || primarySelectedEdge != null) { return; } - const newSelectedEdge = getClosestHoveredBoundingBox(position, planeId); - if (newSelectedEdge != null) { - getSceneController().highlightUserBoundingBox(newSelectedEdge.boxId); - if (newSelectedEdge.direction === "horizontal") { + const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, planeId); + if (hoveredEdgesInfo != null) { + const [primaryHoveredEdge, secondaryHoveredEdge] = hoveredEdgesInfo; + getSceneController().highlightUserBoundingBox(primaryHoveredEdge.boxId); + if (secondaryHoveredEdge != null) { + // If a corner is selected. + body.style.cursor = + (primaryHoveredEdge.isMaxEdge && secondaryHoveredEdge.isMaxEdge) || + (!primaryHoveredEdge.isMaxEdge && !secondaryHoveredEdge.isMaxEdge) + ? "nwse-resize" + : "nesw-resize"; + } else if (primaryHoveredEdge.direction === "horizontal") { body.style.cursor = "row-resize"; } else { body.style.cursor = "col-resize"; @@ -552,24 +561,34 @@ export class BoundingBoxTool { : (_delta: Point2, _position: Point2) => {}; return { leftDownMove: (delta: Point2, pos: Point2, _id: ?string, _event: MouseEvent) => { - if (selectedEdge != null) { - const { didMinAndMaxSwitch } = handleMovingBoundingBox(pos, planeId, selectedEdge); - if (didMinAndMaxSwitch) { - selectedEdge.isMaxEdge = !selectedEdge.isMaxEdge; + if (primarySelectedEdge != null) { + const didMinAndMaxSwitch = handleMovingBoundingBox( + pos, + planeId, + primarySelectedEdge, + secondarySelectedEdge, + ); + if (didMinAndMaxSwitch.primary) { + primarySelectedEdge.isMaxEdge = !primarySelectedEdge.isMaxEdge; + } + if (didMinAndMaxSwitch.secondary && secondarySelectedEdge) { + secondarySelectedEdge.isMaxEdge = !secondarySelectedEdge.isMaxEdge; } } else { MoveHandlers.handleMovePlane(delta); } }, leftMouseDown: (pos: Point2, _plane: OrthoView, _event: MouseEvent) => { - selectedEdge = getClosestHoveredBoundingBox(pos, planeId); - if (selectedEdge) { - getSceneController().highlightUserBoundingBox(selectedEdge.boxId); + const hoveredEdgesInfo = getClosestHoveredBoundingBox(pos, planeId); + if (hoveredEdgesInfo) { + [primarySelectedEdge, secondarySelectedEdge] = hoveredEdgesInfo; + getSceneController().highlightUserBoundingBox(primarySelectedEdge.boxId); } }, leftMouseUp: () => { - selectedEdge = null; + primarySelectedEdge = null; + secondarySelectedEdge = null; getSceneController().highlightUserBoundingBox(null); }, diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.js b/frontend/javascripts/oxalis/view/components/setting_input_views.js index bd5a5408360..f6a47f3648b 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.js @@ -400,9 +400,7 @@ export class UserBoundingBoxInput extends React.PureComponent colorPart * 255): any): Vector3); const iconStyle = { - marginTop: "auto", marginRight: 0, - marginBottom: "auto", marginLeft: 6, }; const exportIconStyle = isExportEnabled diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index a8e96fbf3eb..b04c1547f79 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -401,7 +401,7 @@ function NoNodeContextMenuOptions({ }} > Create new Bounding Box - {isBoundingBoxToolActive ? shortcutBuilder(["leftMouse"]) : null} + {isBoundingBoxToolActive ? shortcutBuilder(["C"]) : null} , ]; if (volumeTracing == null && visibleSegmentationLayer != null) { From 9e1a0cf068157a618c48f0351fb117df1caa240b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Wed, 20 Oct 2021 17:33:14 +0200 Subject: [PATCH 17/34] add delete item to context menu --- frontend/javascripts/oxalis/constants.js | 9 +++++- .../combinations/skeleton_handlers.js | 12 +++++++- .../model/actions/annotation_actions.js | 11 ++++++++ .../model/actions/skeletontracing_actions.js | 1 + .../model/actions/volumetracing_actions.js | 1 + .../model/reducers/annotation_reducer.js | 11 ++++++++ .../javascripts/oxalis/view/context_menu.js | 28 +++++++++++++++++-- .../view/layouting/tracing_layout_view.js | 7 +++++ .../right-border-tabs/bounding_box_tab.js | 17 +++++++++-- 9 files changed, 90 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/oxalis/constants.js b/frontend/javascripts/oxalis/constants.js index 5ab3e12cb94..49005abe342 100644 --- a/frontend/javascripts/oxalis/constants.js +++ b/frontend/javascripts/oxalis/constants.js @@ -233,7 +233,14 @@ export type LabeledVoxelsMap = Map; // e.g., z in XY viewport). export type LabelMasksByBucketAndW = Map>; -export type ShowContextMenuFunction = (number, number, ?number, Vector3, OrthoView) => void; +export type ShowContextMenuFunction = ( + number, + number, + ?number, + ?number, + Vector3, + OrthoView, +) => void; const Constants = { ARBITRARY_VIEW: 4, diff --git a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js index cd51636ca4a..366ab417de0 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.js @@ -46,6 +46,7 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import { renderToTexture } from "oxalis/view/rendering_utils"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import Dimensions from "oxalis/model/dimensions"; +import { getClosestHoveredBoundingBox } from "oxalis/controller/combinations/bounding_box_handlers"; const OrthoViewToNumber: OrthoViewMap = { [OrthoViews.PLANE_XY]: 0, @@ -139,7 +140,16 @@ export function handleOpenContextMenu( const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); const state = Store.getState(); const globalPosition = calculateGlobalPos(state, position); - showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, activeViewport); + const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, plane); + const clickedBoundingBoxId = hoveredEdgesInfo != null ? hoveredEdgesInfo[0].boxId : null; + showNodeContextMenuAt( + event.pageX, + event.pageY, + nodeId, + clickedBoundingBoxId, + globalPosition, + activeViewport, + ); } export function moveNode(dx: number, dy: number, nodeId: ?number) { diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index 074cae163b9..df7cbcdd633 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -50,6 +50,11 @@ type AddNewUserBoundingBox = { center?: Vector3, }; +type DeleteUserBoundingBox = { + type: "DELETE_USER_BOUNDING_BOX", + id: number, +}; + export type UpdateRemoteMeshMetaDataAction = { type: "UPDATE_REMOTE_MESH_METADATA", id: string, @@ -156,6 +161,7 @@ export type AnnotationActionTypes = | UpdateRemoteMeshMetaDataAction | SetUserBoundingBoxesAction | AddNewUserBoundingBox + | DeleteUserBoundingBox | AddUserBoundingBoxesAction | AddMeshMetadataAction | DeleteMeshAction @@ -226,6 +232,11 @@ export const addUserBoundingBoxAction = ( center, }); +export const deleteUserBoundingBoxAction = (id: number): DeleteUserBoundingBox => ({ + type: "DELETE_USER_BOUNDING_BOX", + id, +}); + export const addUserBoundingBoxesAction = ( userBoundingBoxes: Array, ): AddUserBoundingBoxesAction => ({ diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js index 588ac287a84..96e753277f3 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js @@ -201,6 +201,7 @@ export const SkeletonTracingSaveRelevantActions = [ "SET_USER_BOUNDING_BOXES", "ADD_USER_BOUNDING_BOXES", "ADD_NEW_USER_BOUNDING_BOX", + "DELETE_USER_BOUNDING_BOX", "SET_TREE_GROUPS", "SET_TREE_GROUP", "SET_MERGER_MODE_ENABLED", diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index 27ef0fcbea9..4cb1d49bb6c 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -74,6 +74,7 @@ export const VolumeTracingSaveRelevantActions = [ "SET_USER_BOUNDING_BOXES", "ADD_USER_BOUNDING_BOXES", "ADD_NEW_USER_BOUNDING_BOX", + "DELETE_USER_BOUNDING_BOX", "FINISH_ANNOTATION_STROKE", ]; diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 009db171014..b6b3c716c8a 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -132,6 +132,17 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, mergedUserBoundingBoxes); } + case "DELETE_USER_BOUNDING_BOX": { + const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + if (tracing == null) { + return state; + } + const updatedUserBoundingBoxes = tracing.userBoundingBoxes.filter( + bbox => bbox.id !== action.id, + ); + return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + } + case "UPDATE_LOCAL_MESH_METADATA": case "UPDATE_REMOTE_MESH_METADATA": { const { id, meshShape } = action; diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index b04c1547f79..c22f38abf39 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -15,7 +15,10 @@ import type { APIDataset, APIDataLayer } from "types/api_flow_types"; import type { Dispatch } from "redux"; import { connect } from "react-redux"; import { V3 } from "libs/mjs"; -import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; +import { + addUserBoundingBoxAction, + deleteUserBoundingBoxAction, +} from "oxalis/model/actions/annotation_actions"; import { deleteEdgeAction, mergeTreesAction, @@ -51,6 +54,7 @@ import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; type OwnProps = {| contextMenuPosition: [number, number], clickedNodeId: ?number, + clickedBoundingBoxId: ?number, globalPosition: Vector3, viewport: OrthoView, hideContextMenu: () => void, @@ -65,6 +69,7 @@ type DispatchProps = {| createTree: () => void, setActiveCell: number => void, addNewBoundingBox: Vector3 => void, + deleteBoundingBox: number => void, |}; type StateProps = {| @@ -298,10 +303,12 @@ function NoNodeContextMenuOptions({ createTree, segmentIdAtPosition, visibleSegmentationLayer, + clickedBoundingBoxId, dataset, currentMeshFile, setActiveCell, addNewBoundingBox, + deleteBoundingBox, }: NoNodeContextMenuProps) { useEffect(() => { (async () => { @@ -392,7 +399,7 @@ function NoNodeContextMenuOptions({ : []; const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; - const boundingBoxActions = [ + let boundingBoxActions = [ , ]; + if (isBoundingBoxToolActive && clickedBoundingBoxId != null) { + boundingBoxActions = [ + ...boundingBoxActions, + { + deleteBoundingBox(clickedBoundingBoxId); + }} + > + Delete Bounding Box + , + ]; + } if (volumeTracing == null && visibleSegmentationLayer != null) { nonSkeletonActions.push(loadMeshItem); } @@ -585,6 +606,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ addNewBoundingBox(center: Vector3) { dispatch(addUserBoundingBoxAction(null, center)); }, + deleteBoundingBox(id: number) { + dispatch(deleteUserBoundingBoxAction(id)); + }, }); function mapStateToProps(state: OxalisState): StateProps { diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js index ef243f5d288..05fafcf18b2 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js @@ -82,6 +82,7 @@ type State = { status: ControllerStatus, contextMenuPosition: ?[number, number], clickedNodeId: ?number, + clickedBoundingBoxId: ?number, contextMenuGlobalPosition: Vector3, contextMenuViewport: ?OrthoView, model: Object, @@ -111,6 +112,7 @@ class TracingLayoutView extends React.PureComponent { status: "loading", contextMenuPosition: null, clickedNodeId: null, + clickedBoundingBoxId: null, contextMenuGlobalPosition: [0, 0, 0], contextMenuViewport: null, model: layout, @@ -161,6 +163,7 @@ class TracingLayoutView extends React.PureComponent { xPos: number, yPos: number, nodeId: ?number, + boundingBoxId: ?number, globalPosition: Vector3, viewport: OrthoView, ) => { @@ -173,6 +176,7 @@ class TracingLayoutView extends React.PureComponent { this.setState({ contextMenuPosition: [xPos, yPos], clickedNodeId: nodeId, + clickedBoundingBoxId: boundingBoxId, contextMenuGlobalPosition: globalPosition, contextMenuViewport: viewport, }), @@ -184,6 +188,7 @@ class TracingLayoutView extends React.PureComponent { this.setState({ contextMenuPosition: null, clickedNodeId: null, + clickedBoundingBoxId: null, contextMenuGlobalPosition: [0, 0, 0], contextMenuViewport: null, }); @@ -245,6 +250,7 @@ class TracingLayoutView extends React.PureComponent { const { clickedNodeId, + clickedBoundingBoxId, contextMenuPosition, contextMenuGlobalPosition, contextMenuViewport, @@ -277,6 +283,7 @@ class TracingLayoutView extends React.PureComponent { ) => void, addNewBoundingBox: () => void, + deleteBoundingBox: number => void, setPosition: Vector3 => void, dataset: APIDataset, }; function BoundingBoxTab(props: BoundingBoxTabProps) { const [selectedBoundingBoxForExport, setSelectedBoundingBoxForExport] = useState(null); - const { tracing, dataset, onChangeBoundingBoxes, addNewBoundingBox, setPosition } = props; + const { + tracing, + dataset, + onChangeBoundingBoxes, + addNewBoundingBox, + deleteBoundingBox, + setPosition, + } = props; const { userBoundingBoxes } = getSomeTracing(tracing); function handleChangeUserBoundingBox( @@ -80,8 +89,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { } function handleDeleteUserBoundingBox(id: number) { - const updatedUserBoundingBoxes = userBoundingBoxes.filter(boundingBox => boundingBox.id !== id); - onChangeBoundingBoxes(updatedUserBoundingBoxes); + deleteBoundingBox(id); } return ( @@ -145,6 +153,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ setPosition(position: Vector3) { dispatch(setPositionAction(position)); }, + deleteBoundingBox(id: number) { + dispatch(deleteUserBoundingBoxAction(id)); + }, }); export default connect( From 57cfc8be61d93da72cfc216ebba16adabc3a7dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Wed, 20 Oct 2021 18:00:59 +0200 Subject: [PATCH 18/34] add hide bounding box to context menu --- .../oxalis/model/actions/annotation_actions.js | 16 ++++++++++++++++ .../model/actions/skeletontracing_actions.js | 1 + .../model/actions/volumetracing_actions.js | 1 + .../oxalis/model/reducers/annotation_reducer.js | 11 +++++++++++ .../view/components/setting_input_views.js | 7 ++----- frontend/javascripts/oxalis/view/context_menu.js | 15 +++++++++++++++ .../view/right-border-tabs/bounding_box_tab.js | 11 +++++++++++ 7 files changed, 57 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index df7cbcdd633..b17e688d6c2 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -50,6 +50,12 @@ type AddNewUserBoundingBox = { center?: Vector3, }; +type SetUserBoundingBoxVisibilityAction = { + type: "SET_USER_BOUNDING_BOX_VISIBILITY", + id: number, + isVisible: boolean, +}; + type DeleteUserBoundingBox = { type: "DELETE_USER_BOUNDING_BOX", id: number, @@ -161,6 +167,7 @@ export type AnnotationActionTypes = | UpdateRemoteMeshMetaDataAction | SetUserBoundingBoxesAction | AddNewUserBoundingBox + | SetUserBoundingBoxVisibilityAction | DeleteUserBoundingBox | AddUserBoundingBoxesAction | AddMeshMetadataAction @@ -232,6 +239,15 @@ export const addUserBoundingBoxAction = ( center, }); +export const setUserBoundingBoxVisibilityAction = ( + id: number, + isVisible: boolean, +): SetUserBoundingBoxVisibilityAction => ({ + type: "SET_USER_BOUNDING_BOX_VISIBILITY", + id, + isVisible, +}); + export const deleteUserBoundingBoxAction = (id: number): DeleteUserBoundingBox => ({ type: "DELETE_USER_BOUNDING_BOX", id, diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js index 96e753277f3..8a818e0afc6 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js @@ -202,6 +202,7 @@ export const SkeletonTracingSaveRelevantActions = [ "ADD_USER_BOUNDING_BOXES", "ADD_NEW_USER_BOUNDING_BOX", "DELETE_USER_BOUNDING_BOX", + "SET_USER_BOUNDING_BOX_VISIBILITY", "SET_TREE_GROUPS", "SET_TREE_GROUP", "SET_MERGER_MODE_ENABLED", diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index 4cb1d49bb6c..f11e5b86e35 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -75,6 +75,7 @@ export const VolumeTracingSaveRelevantActions = [ "ADD_USER_BOUNDING_BOXES", "ADD_NEW_USER_BOUNDING_BOX", "DELETE_USER_BOUNDING_BOX", + "SET_USER_BOUNDING_BOX_VISIBILITY", "FINISH_ANNOTATION_STROKE", ]; diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index b6b3c716c8a..9d88db00c3e 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -143,6 +143,17 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); } + case "SET_USER_BOUNDING_BOX_VISIBILITY": { + const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + if (tracing == null) { + return state; + } + const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => + bbox.id !== action.id ? bbox : { ...bbox, isVisible: action.isVisible }, + ); + return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + } + case "UPDATE_LOCAL_MESH_METADATA": case "UPDATE_REMOTE_MESH_METADATA": { const { id, meshShape } = action; diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.js b/frontend/javascripts/oxalis/view/components/setting_input_views.js index f6a47f3648b..293738178d9 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.js @@ -305,6 +305,7 @@ type UserBoundingBoxInputProps = { onDelete: () => void, onExport: () => void, onGoToBoundingBox: () => void, + onVisibilityChange: boolean => void, }; type State = { @@ -375,10 +376,6 @@ export class UserBoundingBoxInput extends React.PureComponent { - this.props.onChange({ isVisible }); - }; - handleNameChanged = (evt: SyntheticInputEvent<>) => { const currentEnteredName = evt.target.value; if (currentEnteredName !== this.props.name) { @@ -422,7 +419,7 @@ export class UserBoundingBoxInput extends React.PureComponent diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index c22f38abf39..8e4bf5d57fa 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -18,6 +18,7 @@ import { V3 } from "libs/mjs"; import { addUserBoundingBoxAction, deleteUserBoundingBoxAction, + setUserBoundingBoxVisibilityAction, } from "oxalis/model/actions/annotation_actions"; import { deleteEdgeAction, @@ -67,6 +68,7 @@ type DispatchProps = {| setActiveNode: number => void, hideTree: number => void, createTree: () => void, + hideBoundingBox: number => void, setActiveCell: number => void, addNewBoundingBox: Vector3 => void, deleteBoundingBox: number => void, @@ -308,6 +310,7 @@ function NoNodeContextMenuOptions({ currentMeshFile, setActiveCell, addNewBoundingBox, + hideBoundingBox, deleteBoundingBox, }: NoNodeContextMenuProps) { useEffect(() => { @@ -414,6 +417,15 @@ function NoNodeContextMenuOptions({ if (isBoundingBoxToolActive && clickedBoundingBoxId != null) { boundingBoxActions = [ ...boundingBoxActions, + { + hideBoundingBox(clickedBoundingBoxId); + }} + > + Hide Bounding Box + , ) => ({ deleteBoundingBox(id: number) { dispatch(deleteUserBoundingBoxAction(id)); }, + hideBoundingBox(id: number) { + dispatch(setUserBoundingBoxVisibilityAction(id, false)); + }, }); function mapStateToProps(state: OxalisState): StateProps { diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js index 28712876801..1fc8fb25805 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js @@ -22,6 +22,7 @@ import { setUserBoundingBoxesAction, addUserBoundingBoxAction, deleteUserBoundingBoxAction, + setUserBoundingBoxVisibilityAction, } from "oxalis/model/actions/annotation_actions"; import * as Utils from "libs/utils"; @@ -32,6 +33,7 @@ type BoundingBoxTabProps = { onChangeBoundingBoxes: (value: Array) => void, addNewBoundingBox: () => void, deleteBoundingBox: number => void, + setBoundingBoxVisibility: (number, boolean) => void, setPosition: Vector3 => void, dataset: APIDataset, }; @@ -43,6 +45,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { dataset, onChangeBoundingBoxes, addNewBoundingBox, + setBoundingBoxVisibility, deleteBoundingBox, setPosition, } = props; @@ -70,6 +73,10 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { onChangeBoundingBoxes(updatedUserBoundingBoxes); } + function handleBoundingBoxVisibilityChange(id: number, isVisible: boolean) { + setBoundingBoxVisibility(id, isVisible); + } + function handleGoToBoundingBox(id: number) { const boundingBoxEntry = userBoundingBoxes.find(bbox => bbox.id === id); if (!boundingBoxEntry) { @@ -110,6 +117,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { dataset.jobsEnabled ? _.partial(setSelectedBoundingBoxForExport, bb) : () => {} } onGoToBoundingBox={_.partial(handleGoToBoundingBox, bb.id)} + onVisibilityChange={_.partial(handleBoundingBoxVisibilityChange, bb.id)} /> )) ) : ( @@ -156,6 +164,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ deleteBoundingBox(id: number) { dispatch(deleteUserBoundingBoxAction(id)); }, + setBoundingBoxVisibility(id: number, isVisible: boolean) { + dispatch(setUserBoundingBoxVisibilityAction(id, isVisible)); + }, }); export default connect( From a0ea72d670aa371f5b301e9a9388cb5fa4aa2015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 21 Oct 2021 17:10:26 +0200 Subject: [PATCH 19/34] add change name and change color of bbox to context menu --- .../model/accessors/tracing_accessor.js | 13 ++ .../model/actions/annotation_actions.js | 32 ++++ .../model/reducers/annotation_reducer.js | 32 +++- .../view/components/setting_input_views.js | 6 +- .../javascripts/oxalis/view/context_menu.js | 147 ++++++++++++++---- .../right-border-tabs/bounding_box_tab.js | 22 +++ 6 files changed, 219 insertions(+), 33 deletions(-) diff --git a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js index 723659a7c12..a93122f9a0d 100644 --- a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js @@ -9,6 +9,19 @@ import type { import { TracingTypeEnum } from "types/api_flow_types"; import type { Tracing, VolumeTracing, SkeletonTracing, ReadOnlyTracing } from "oxalis/store"; +export function maybeGetSomeTracing( + tracing: Tracing, +): SkeletonTracing | VolumeTracing | ReadOnlyTracing | null { + if (tracing.skeleton != null) { + return tracing.skeleton; + } else if (tracing.volume != null) { + return tracing.volume; + } else if (tracing.readOnly != null) { + return tracing.readOnly; + } + return null; +} + export function getSomeTracing( tracing: Tracing, ): SkeletonTracing | VolumeTracing | ReadOnlyTracing { diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index b17e688d6c2..b9d088ceb92 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -56,6 +56,18 @@ type SetUserBoundingBoxVisibilityAction = { isVisible: boolean, }; +type SetUserBoundingBoxNameAction = { + type: "SET_USER_BOUNDING_BOX_NAME", + id: number, + name: string, +}; + +type SetUserBoundingBoxColorAction = { + type: "SET_USER_BOUNDING_BOX_COLOR", + id: number, + color: Vector3, +}; + type DeleteUserBoundingBox = { type: "DELETE_USER_BOUNDING_BOX", id: number, @@ -168,6 +180,8 @@ export type AnnotationActionTypes = | SetUserBoundingBoxesAction | AddNewUserBoundingBox | SetUserBoundingBoxVisibilityAction + | SetUserBoundingBoxNameAction + | SetUserBoundingBoxColorAction | DeleteUserBoundingBox | AddUserBoundingBoxesAction | AddMeshMetadataAction @@ -248,6 +262,24 @@ export const setUserBoundingBoxVisibilityAction = ( isVisible, }); +export const setUserBoundingBoxNameAction = ( + id: number, + name: string, +): SetUserBoundingBoxNameAction => ({ + type: "SET_USER_BOUNDING_BOX_NAME", + id, + name, +}); + +export const setUserBoundingBoxColorAction = ( + id: number, + color: Vector3, +): SetUserBoundingBoxColorAction => ({ + type: "SET_USER_BOUNDING_BOX_COLOR", + id, + color, +}); + export const deleteUserBoundingBoxAction = (id: number): DeleteUserBoundingBox => ({ type: "DELETE_USER_BOUNDING_BOX", id, diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 9d88db00c3e..f33f4da9f5a 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -10,6 +10,7 @@ import { updateKey2, updateKey3, } from "oxalis/model/helpers/deep_update"; +import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import * as Utils from "libs/utils"; import { getDisplayedDataExtentInPlaneMode } from "oxalis/model/accessors/view_mode_accessor"; import { map3 } from "libs/utils"; @@ -75,7 +76,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } case "ADD_NEW_USER_BOUNDING_BOX": { - const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + const tracing = maybeGetSomeTracing(state.tracing); if (tracing == null) { return state; } @@ -116,7 +117,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } case "ADD_USER_BOUNDING_BOXES": { - const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + const tracing = maybeGetSomeTracing(state.tracing); if (tracing == null) { return state; } @@ -133,7 +134,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } case "DELETE_USER_BOUNDING_BOX": { - const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + const tracing = maybeGetSomeTracing(state.tracing); if (tracing == null) { return state; } @@ -144,7 +145,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } case "SET_USER_BOUNDING_BOX_VISIBILITY": { - const tracing = state.tracing.skeleton || state.tracing.volume || state.tracing.readOnly; + const tracing = maybeGetSomeTracing(state.tracing); if (tracing == null) { return state; } @@ -154,7 +155,28 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); } - case "UPDATE_LOCAL_MESH_METADATA": + case "SET_USER_BOUNDING_BOX_NAME": { + const tracing = maybeGetSomeTracing(state.tracing); + if (tracing == null) { + return state; + } + const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => + bbox.id !== action.id ? bbox : { ...bbox, name: action.name }, + ); + return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + } + + case "SET_USER_BOUNDING_BOX_COLOR": { + const tracing = maybeGetSomeTracing(state.tracing); + if (tracing == null) { + return state; + } + const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => + bbox.id !== action.id ? bbox : { ...bbox, color: action.color }, + ); + return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + } + case "UPDATE_REMOTE_MESH_METADATA": { const { id, meshShape } = action; const newMeshes = state.tracing.meshes.map(mesh => { diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.js b/frontend/javascripts/oxalis/view/components/setting_input_views.js index 293738178d9..66cff6f8e92 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.js @@ -306,6 +306,8 @@ type UserBoundingBoxInputProps = { onExport: () => void, onGoToBoundingBox: () => void, onVisibilityChange: boolean => void, + onNameChange: string => void, + onColorChange: Vector3 => void, }; type State = { @@ -373,13 +375,13 @@ export class UserBoundingBoxInput extends React.PureComponent { color = ((color.map(colorPart => colorPart / 255): any): Vector3); - this.props.onChange({ color }); + this.props.onColorChange(color); }; handleNameChanged = (evt: SyntheticInputEvent<>) => { const currentEnteredName = evt.target.value; if (currentEnteredName !== this.props.name) { - this.props.onChange({ name: evt.target.value }); + this.props.onNameChange(currentEnteredName); } }; diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index 8e4bf5d57fa..d420d49cb6a 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -1,6 +1,6 @@ // @flow import React, { useEffect, type Node } from "react"; -import { Menu, notification, Divider, Tooltip } from "antd"; +import { Menu, notification, Divider, Tooltip, Popover, Input } from "antd"; import { CopyOutlined } from "@ant-design/icons"; import { type AnnotationTool, @@ -10,6 +10,7 @@ import { VolumeTools, } from "oxalis/constants"; +import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import type { OxalisState, SkeletonTracing, VolumeTracing } from "oxalis/store"; import type { APIDataset, APIDataLayer } from "types/api_flow_types"; import type { Dispatch } from "redux"; @@ -18,8 +19,11 @@ import { V3 } from "libs/mjs"; import { addUserBoundingBoxAction, deleteUserBoundingBoxAction, + setUserBoundingBoxNameAction, + setUserBoundingBoxColorAction, setUserBoundingBoxVisibilityAction, } from "oxalis/model/actions/annotation_actions"; +import { type UserBoundingBox } from "oxalis/store"; import { deleteEdgeAction, mergeTreesAction, @@ -45,7 +49,7 @@ import messages from "messages"; import { getVisibleSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; import { getNodeAndTree, findTreeByNodeId } from "oxalis/model/accessors/skeletontracing_accessor"; import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils"; -import { roundTo } from "libs/utils"; +import { roundTo, hexToRgb, rgbToHex } from "libs/utils"; import Shortcut from "libs/shortcut_component"; import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; @@ -71,6 +75,9 @@ type DispatchProps = {| hideBoundingBox: number => void, setActiveCell: number => void, addNewBoundingBox: Vector3 => void, + setBoundingBoxColor: (number, Vector3) => void, + setBoundingBoxName: (number, string) => void, + addNewBoundingBox: Vector3 => void, deleteBoundingBox: number => void, |}; @@ -84,6 +91,7 @@ type StateProps = {| volumeTracing: ?VolumeTracing, activeTool: AnnotationTool, useLegacyBindings: boolean, + userBoundingBoxes: Array | null, |}; /* eslint-enable react/no-unused-prop-types */ @@ -309,8 +317,11 @@ function NoNodeContextMenuOptions({ dataset, currentMeshFile, setActiveCell, + userBoundingBoxes, addNewBoundingBox, hideBoundingBox, + setBoundingBoxName, + setBoundingBoxColor, deleteBoundingBox, }: NoNodeContextMenuProps) { useEffect(() => { @@ -414,28 +425,96 @@ function NoNodeContextMenuOptions({ {isBoundingBoxToolActive ? shortcutBuilder(["C"]) : null} , ]; - if (isBoundingBoxToolActive && clickedBoundingBoxId != null) { - boundingBoxActions = [ - ...boundingBoxActions, - { - hideBoundingBox(clickedBoundingBoxId); - }} - > - Hide Bounding Box - , - { - deleteBoundingBox(clickedBoundingBoxId); - }} - > - Delete Bounding Box - , - ]; + if (isBoundingBoxToolActive && clickedBoundingBoxId != null && userBoundingBoxes != null) { + const hoveredBBox = userBoundingBoxes.find(bbox => bbox.id === clickedBoundingBoxId); + if (hoveredBBox) { + const setBBoxName = (evt: SyntheticInputEvent<>) => { + setBoundingBoxName(clickedBoundingBoxId, evt.target.value); + }; + const preventContextMenuFromClosing = evt => { + evt.stopPropagation(); + }; + const upscaledBBoxColor = ((hoveredBBox.color.map( + colorPart => colorPart * 255, + ): any): Vector3); + boundingBoxActions = [ + ...boundingBoxActions, + + { + setBBoxName(evt); + hideContextMenu(); + }} + onBlur={setBBoxName} + onClick={preventContextMenuFromClosing} + /> + } + trigger="click" + > + + Change Bounding Box Name + + + , + + + Change Bounding Box Color + ) => { + let color = hexToRgb(evt.target.value); + color = ((color.map(colorPart => colorPart / 255): any): Vector3); + setBoundingBoxColor(clickedBoundingBoxId, color); + }} + onBlur={() => hideContextMenu} + value={rgbToHex(upscaledBBoxColor)} + /> + + , + { + hideBoundingBox(clickedBoundingBoxId); + }} + > + Hide Bounding Box + , + { + deleteBoundingBox(clickedBoundingBoxId); + }} + > + Delete Bounding Box + , + ]; + } } if (volumeTracing == null && visibleSegmentationLayer != null) { nonSkeletonActions.push(loadMeshItem); @@ -454,8 +533,17 @@ function NoNodeContextMenuOptions({ return null; } + // const me + return ( - + { + console.log("hiding context menu", args); + hideContextMenu(); + }} + style={{ borderRadius: 6 }} + mode="vertical" + > {allActions} ); @@ -618,6 +706,12 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ addNewBoundingBox(center: Vector3) { dispatch(addUserBoundingBoxAction(null, center)); }, + setBoundingBoxName(id: number, name: string) { + dispatch(setUserBoundingBoxNameAction(id, name)); + }, + setBoundingBoxColor(id: number, color: Vector3) { + dispatch(setUserBoundingBoxColorAction(id, color)); + }, deleteBoundingBox(id: number) { dispatch(deleteUserBoundingBoxAction(id)); }, @@ -628,7 +722,7 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ function mapStateToProps(state: OxalisState): StateProps { const visibleSegmentationLayer = getVisibleSegmentationLayer(state); - + const someTracing = maybeGetSomeTracing(state.tracing); return { skeletonTracing: state.tracing.skeleton, volumeTracing: state.tracing.volume, @@ -642,6 +736,7 @@ function mapStateToProps(state: OxalisState): StateProps { ? state.currentMeshFileByLayer[visibleSegmentationLayer.name] : null, useLegacyBindings: state.userConfiguration.useLegacyBindings, + userBoundingBoxes: someTracing != null ? someTracing.userBoundingBoxes : null, }; } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js index 1fc8fb25805..e02a52d4f43 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js @@ -23,6 +23,8 @@ import { addUserBoundingBoxAction, deleteUserBoundingBoxAction, setUserBoundingBoxVisibilityAction, + setUserBoundingBoxNameAction, + setUserBoundingBoxColorAction, } from "oxalis/model/actions/annotation_actions"; import * as Utils from "libs/utils"; @@ -34,6 +36,8 @@ type BoundingBoxTabProps = { addNewBoundingBox: () => void, deleteBoundingBox: number => void, setBoundingBoxVisibility: (number, boolean) => void, + setBoundingBoxName: (number, string) => void, + setBoundingBoxColor: (number, Vector3) => void, setPosition: Vector3 => void, dataset: APIDataset, }; @@ -46,6 +50,8 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { onChangeBoundingBoxes, addNewBoundingBox, setBoundingBoxVisibility, + setBoundingBoxName, + setBoundingBoxColor, deleteBoundingBox, setPosition, } = props; @@ -77,6 +83,14 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { setBoundingBoxVisibility(id, isVisible); } + function handleBoundingBoxNameChange(id: number, name: string) { + setBoundingBoxName(id, name); + } + + function handleBoundingBoxColorChange(id: number, color: Vector3) { + setBoundingBoxColor(id, color); + } + function handleGoToBoundingBox(id: number) { const boundingBoxEntry = userBoundingBoxes.find(bbox => bbox.id === id); if (!boundingBoxEntry) { @@ -118,6 +132,8 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { } onGoToBoundingBox={_.partial(handleGoToBoundingBox, bb.id)} onVisibilityChange={_.partial(handleBoundingBoxVisibilityChange, bb.id)} + onNameChange={_.partial(handleBoundingBoxNameChange, bb.id)} + onColorChange={_.partial(handleBoundingBoxColorChange, bb.id)} /> )) ) : ( @@ -167,6 +183,12 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ setBoundingBoxVisibility(id: number, isVisible: boolean) { dispatch(setUserBoundingBoxVisibilityAction(id, isVisible)); }, + setBoundingBoxName(id: number, name: string) { + dispatch(setUserBoundingBoxNameAction(id, name)); + }, + setBoundingBoxColor(id: number, color: Vector3) { + dispatch(setUserBoundingBoxColorAction(id, color)); + }, }); export default connect( From 9aefac9bcc28f25d0df06f3b2c93ee39b4b0adb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 21 Oct 2021 18:06:47 +0200 Subject: [PATCH 20/34] WIP: Adding BBox undo/redo --- .../model/actions/annotation_actions.js | 19 ++++++ .../model/actions/skeletontracing_actions.js | 7 +- .../model/actions/volumetracing_actions.js | 7 +- .../oxalis/model/sagas/save_saga.js | 67 +++++++++++++++++-- 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index b9d088ceb92..65136ee3de0 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -202,6 +202,25 @@ export type AnnotationActionTypes = | RemoveIsosurfaceAction | AddIsosurfaceAction; +export type UserBoundingBoxAction = + | SetUserBoundingBoxesAction + | AddNewUserBoundingBox + | SetUserBoundingBoxVisibilityAction + | SetUserBoundingBoxNameAction + | SetUserBoundingBoxColorAction + | DeleteUserBoundingBox + | AddUserBoundingBoxesAction; + +export const AllUserBoundingBoxActions = [ + "SET_USER_BOUNDING_BOXES", + "ADD_NEW_USER_BOUNDING_BOX", + "SET_USER_BOUNDING_BOX_VISIBILITY", + "SET_USER_BOUNDING_BOX_NAME", + "SET_USER_BOUNDING_BOX_COLOR", + "DELETE_USER_BOUNDING_BOX", + "ADD_USER_BOUNDING_BOXES", +]; + export const initializeAnnotationAction = ( annotation: APIAnnotation, ): InitializeAnnotationAction => ({ diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js index 8a818e0afc6..5412e46568c 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js @@ -18,6 +18,7 @@ import Store, { } from "oxalis/store"; import messages from "messages"; import renderIndependently from "libs/render_independently"; +import { AllUserBoundingBoxActions } from "oxalis/model/actions/annotation_actions"; type InitializeSkeletonTracingAction = { type: "INITIALIZE_SKELETONTRACING", @@ -198,11 +199,6 @@ export const SkeletonTracingSaveRelevantActions = [ "SHUFFLE_ALL_TREE_COLORS", "CREATE_COMMENT", "DELETE_COMMENT", - "SET_USER_BOUNDING_BOXES", - "ADD_USER_BOUNDING_BOXES", - "ADD_NEW_USER_BOUNDING_BOX", - "DELETE_USER_BOUNDING_BOX", - "SET_USER_BOUNDING_BOX_VISIBILITY", "SET_TREE_GROUPS", "SET_TREE_GROUP", "SET_MERGER_MODE_ENABLED", @@ -213,6 +209,7 @@ export const SkeletonTracingSaveRelevantActions = [ "SET_TREE_COLOR", // Composited actions, only dispatched using `batchActions` "DELETE_GROUP_AND_TREES", + ...AllUserBoundingBoxActions, ]; const noAction = (): NoAction => ({ diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index f11e5b86e35..8bb35878d53 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -7,6 +7,7 @@ import type { Vector2, Vector3, Vector4, OrthoView, ContourMode } from "oxalis/c import type { BucketDataArray } from "oxalis/model/bucket_data_handling/bucket"; import Deferred from "libs/deferred"; import { type Dispatch } from "redux"; +import { AllUserBoundingBoxActions } from "oxalis/model/actions/annotation_actions"; type InitializeVolumeTracingAction = { type: "INITIALIZE_VOLUMETRACING", @@ -71,12 +72,8 @@ export type VolumeTracingAction = export const VolumeTracingSaveRelevantActions = [ "CREATE_CELL", "SET_ACTIVE_CELL", - "SET_USER_BOUNDING_BOXES", - "ADD_USER_BOUNDING_BOXES", - "ADD_NEW_USER_BOUNDING_BOX", - "DELETE_USER_BOUNDING_BOX", - "SET_USER_BOUNDING_BOX_VISIBILITY", "FINISH_ANNOTATION_STROKE", + ...AllUserBoundingBoxActions, ]; export const VolumeTracingUndoRelevantActions = ["START_EDITING", "COPY_SEGMENTATION_LAYER"]; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index dc1e3a9e854..edf9b8b4db7 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -8,6 +8,7 @@ import Maybe from "data.maybe"; import type { Action } from "oxalis/model/actions/actions"; import { FlycamActions } from "oxalis/model/actions/flycam_actions"; +import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import { PUSH_THROTTLE_TIME, SAVE_RETRY_WAITING_TIME, @@ -21,7 +22,14 @@ import { setTracingAction, centerActiveNodeAction, } from "oxalis/model/actions/skeletontracing_actions"; -import type { Tracing, SkeletonTracing, Flycam, SaveQueueEntry, CameraData } from "oxalis/store"; +import type { + Tracing, + SkeletonTracing, + Flycam, + SaveQueueEntry, + CameraData, + UserBoundingBox, +} from "oxalis/store"; import createProgressCallback from "libs/progress_callback"; import { setBusyBlockingInfoAction } from "oxalis/model/actions/ui_actions"; import { @@ -76,6 +84,10 @@ import compactSaveQueue from "oxalis/model/helpers/compaction/compact_save_queue import compactUpdateActions from "oxalis/model/helpers/compaction/compact_update_actions"; import compressLz4Block from "oxalis/workers/byte_array_lz4_compression.worker"; import messages from "messages"; +import { + AllUserBoundingBoxActions, + type UserBoundingBoxAction, +} from "oxalis/model/actions/annotation_actions"; import window, { alert, document, location } from "libs/window"; import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; @@ -91,13 +103,15 @@ type UndoBucket = { type VolumeAnnotationBatch = Array; type SkeletonUndoState = { type: "skeleton", data: SkeletonTracing }; type VolumeUndoState = { type: "volume", data: VolumeAnnotationBatch }; +type BoundingBoxUndoState = { type: "bounding box", data: Array }; type WarnUndoState = { type: "warning", reason: string }; -type UndoState = SkeletonUndoState | VolumeUndoState | WarnUndoState; +type UndoState = SkeletonUndoState | VolumeUndoState | BoundingBoxUndoState | WarnUndoState; type RelevantActionsForUndoRedo = { skeletonUserAction?: SkeletonTracingAction, addBucketToUndoAction?: AddBucketToUndoAction, finishAnnotationStrokeAction?: FinishAnnotationStrokeAction, + userBoundingBoxAction?: UserBoundingBoxAction, importVolumeTracingAction?: ImportVolumeTracingAction, undo?: UndoAction, redo?: RedoAction, @@ -132,33 +146,42 @@ function unpackRelevantActionForUndo(action): RelevantActionsForUndoRedo { } if (SkeletonTracingSaveRelevantActions.includes(action.type)) { - return { - skeletonUserAction: ((action: any): SkeletonTracingAction), - }; + return { skeletonUserAction: ((action: any): SkeletonTracingAction) }; } + if (AllUserBoundingBoxActions.includes(action.type)) { + return { userBoundingBoxAction: ((action: any): UserBoundingBoxAction) }; + } throw new Error("Could not unpack redux action from channel"); } +const getUserBoundingBoxesFromState = state => { + const maybeSomeTracing = maybeGetSomeTracing(state.tracing); + return maybeSomeTracing != null ? maybeSomeTracing.userBoundingBoxes : []; +}; + export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; let previousAction: ?any = null; let prevSkeletonTracingOrNull: ?SkeletonTracing = null; + let prevUserBoundingBoxes: Array = []; let pendingCompressions: Array> = []; let currentVolumeAnnotationBatch: VolumeAnnotationBatch = []; yield* take(["INITIALIZE_SKELETONTRACING", "INITIALIZE_VOLUMETRACING"]); prevSkeletonTracingOrNull = yield* select(state => state.tracing.skeleton); - + prevUserBoundingBoxes = yield* select(getUserBoundingBoxesFromState); const actionChannel = yield _actionChannel([ ...SkeletonTracingSaveRelevantActions, + ...AllUserBoundingBoxActions, "ADD_BUCKET_TO_UNDO", "FINISH_ANNOTATION_STROKE", "IMPORT_VOLUMETRACING", "UNDO", "REDO", ]); + // TODO: Just trigger an action when dragging and when stopped dragging to be able to batch the actions while (true) { const currentAction = yield* take(actionChannel); @@ -166,12 +189,18 @@ export function* collectUndoStates(): Saga { skeletonUserAction, addBucketToUndoAction, finishAnnotationStrokeAction, + userBoundingBoxAction, importVolumeTracingAction, undo, redo, } = unpackRelevantActionForUndo(currentAction); - if (skeletonUserAction || addBucketToUndoAction || finishAnnotationStrokeAction) { + if ( + skeletonUserAction || + addBucketToUndoAction || + finishAnnotationStrokeAction || + userBoundingBoxAction + ) { let shouldClearRedoState = addBucketToUndoAction != null || finishAnnotationStrokeAction != null; if (skeletonUserAction && prevSkeletonTracingOrNull != null) { @@ -203,6 +232,18 @@ export function* collectUndoStates(): Saga { undoStack.push({ type: "volume", data: currentVolumeAnnotationBatch }); currentVolumeAnnotationBatch = []; pendingCompressions = []; + } else if (userBoundingBoxAction) { + const boundingBoxUndoState = yield* call( + getBoundingBoxToUndoState, + userBoundingBoxAction, + prevSkeletonTracingOrNull, + previousAction, + ); + if (skeletonUndoState) { + shouldClearRedoState = true; + undoStack.push(skeletonUndoState); + } + previousAction = skeletonUserAction; } if (shouldClearRedoState) { // Clear the redo stack when a new action is executed. @@ -255,6 +296,18 @@ function* getSkeletonTracingToUndoState( return null; } +function* getBoundingBoxToUndoState( + userBoundingBoxAction: UserBoundingBoxAction, + prevUserBoundingBoxes: Array, + previousAction: ?SkeletonTracingAction, +): Saga { + const currentUserBoundingBoxes = yield* select(getUserBoundingBoxesFromState); + if (shouldAddToUndoStack(userBoundingBoxAction, previousAction)) { + return { type: "bounding box", data: prevUserBoundingBoxes }; + } + return null; +} + function* compressBucketAndAppendTo( zoomedBucketAddress: Vector4, bucketData: BucketDataArray, From 1ca5f277a8299f31b55b0732bd6459907303d5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 22 Oct 2021 13:38:42 +0200 Subject: [PATCH 21/34] add bounding box undo / redo --- .../combinations/bounding_box_handlers.js | 77 ++++++++--------- .../controller/combinations/tool_controls.js | 8 +- .../model/actions/annotation_actions.js | 34 +++++++- .../model/reducers/annotation_reducer.js | 16 ++++ .../oxalis/model/sagas/save_saga.js | 82 +++++++++++++------ .../view/components/setting_input_views.js | 11 +-- .../right-border-tabs/bounding_box_tab.js | 43 +++------- 7 files changed, 162 insertions(+), 109 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index f7d777d7526..e5c09ddd514 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -5,7 +5,7 @@ import { type OrthoView, type Point2, type Vector3 } from "oxalis/constants"; import Store from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import Dimension from "oxalis/model/dimensions"; -import { setUserBoundingBoxesAction } from "oxalis/model/actions/annotation_actions"; +import { setUserBoundingBoxBoundsAction } from "oxalis/model/actions/annotation_actions"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; const getNeighbourEdgeIndexByEdgeIndex = { @@ -195,52 +195,53 @@ export function getClosestHoveredBoundingBox( return [primaryEdge, secondaryEdge]; } -export function handleMovingBoundingBox( +export function handleResizingBoundingBox( mousePosition: Point2, planeId: OrthoView, primaryEdge: SelectedEdge, secondaryEdge: ?SelectedEdge, -) { +): { primary: boolean, secondary: boolean } { const state = Store.getState(); const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId); const { userBoundingBoxes } = getSomeTracing(state.tracing); const didMinAndMaxSwitch = { primary: false, secondary: false }; - const updatedUserBoundingBoxes = userBoundingBoxes.map(bbox => { - if (bbox.id !== primaryEdge.boxId) { - return bbox; - } - bbox = _.cloneDeep(bbox); - function applyPositionToEdge(edge: SelectedEdge): boolean { - const { resizableDimension } = edge; - // For a horizontal edge only consider delta.y, for vertical only delta.x - const newPositionValue = Math.round(globalMousePosition[resizableDimension]); - const minOrMax = edge.isMaxEdge ? "max" : "min"; - const oppositeOfMinOrMax = edge.isMaxEdge ? "min" : "max"; - const otherEdgeValue = bbox.boundingBox[oppositeOfMinOrMax][resizableDimension]; - if (otherEdgeValue === newPositionValue) { - // Do not allow the same value for min and max for one dimension. - return false; - } - const areMinAndMaxEdgeCrossing = - // If the min / max edge is moved over the other one. - (edge.isMaxEdge && newPositionValue < otherEdgeValue) || - (!edge.isMaxEdge && newPositionValue > otherEdgeValue); - if (areMinAndMaxEdgeCrossing) { - // As the edge moved over the other one, the values for min and max must be switched. - bbox.boundingBox[minOrMax][resizableDimension] = otherEdgeValue; - bbox.boundingBox[oppositeOfMinOrMax][resizableDimension] = newPositionValue; - return true; - } else { - bbox.boundingBox[minOrMax][resizableDimension] = newPositionValue; - return false; - } + const bboxToResize = userBoundingBoxes.find(bbox => bbox.id === primaryEdge.boxId); + if (!bboxToResize) { + return didMinAndMaxSwitch; + } + const updatedBounds = { + min: [...bboxToResize.boundingBox.min], + max: [...bboxToResize.boundingBox.max], + }; + function updateBoundsAccordingToEdge(edge: SelectedEdge): boolean { + const { resizableDimension } = edge; + // For a horizontal edge only consider delta.y, for vertical only delta.x + const newPositionValue = Math.round(globalMousePosition[resizableDimension]); + const minOrMax = edge.isMaxEdge ? "max" : "min"; + const oppositeOfMinOrMax = edge.isMaxEdge ? "min" : "max"; + const otherEdgeValue = bboxToResize.boundingBox[oppositeOfMinOrMax][resizableDimension]; + if (otherEdgeValue === newPositionValue) { + // Do not allow the same value for min and max for one dimension. + return false; } - didMinAndMaxSwitch.primary = applyPositionToEdge(primaryEdge); - if (secondaryEdge) { - didMinAndMaxSwitch.secondary = applyPositionToEdge(secondaryEdge); + const areMinAndMaxEdgeCrossing = + // If the min / max edge is moved over the other one. + (edge.isMaxEdge && newPositionValue < otherEdgeValue) || + (!edge.isMaxEdge && newPositionValue > otherEdgeValue); + if (areMinAndMaxEdgeCrossing) { + // As the edge moved over the other one, the values for min and max must be switched. + updatedBounds[minOrMax][resizableDimension] = otherEdgeValue; + updatedBounds[oppositeOfMinOrMax][resizableDimension] = newPositionValue; + return true; + } else { + updatedBounds[minOrMax][resizableDimension] = newPositionValue; + return false; } - return bbox; - }); - Store.dispatch(setUserBoundingBoxesAction(updatedUserBoundingBoxes)); + } + didMinAndMaxSwitch.primary = updateBoundsAccordingToEdge(primaryEdge); + if (secondaryEdge) { + didMinAndMaxSwitch.secondary = updateBoundsAccordingToEdge(secondaryEdge); + } + Store.dispatch(setUserBoundingBoxBoundsAction(primaryEdge.boxId, updatedBounds)); return didMinAndMaxSwitch; } diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index b0083a9bfa4..fb0c988e4bc 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -18,13 +18,14 @@ import { handleAgglomerateSkeletonAtClick } from "oxalis/controller/combinations import { hideBrushAction } from "oxalis/model/actions/volumetracing_actions"; import { isBrushTool } from "oxalis/model/accessors/tool_accessor"; import getSceneController from "oxalis/controller/scene_controller_provider"; +import { finishedResizingUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import * as MoveHandlers from "oxalis/controller/combinations/move_handlers"; import PlaneView from "oxalis/view/plane_view"; import * as SkeletonHandlers from "oxalis/controller/combinations/skeleton_handlers"; import { type SelectedEdge, getClosestHoveredBoundingBox, - handleMovingBoundingBox, + handleResizingBoundingBox, } from "oxalis/controller/combinations/bounding_box_handlers"; import Store from "oxalis/store"; import * as Utils from "libs/utils"; @@ -562,7 +563,7 @@ export class BoundingBoxTool { return { leftDownMove: (delta: Point2, pos: Point2, _id: ?string, _event: MouseEvent) => { if (primarySelectedEdge != null) { - const didMinAndMaxSwitch = handleMovingBoundingBox( + const didMinAndMaxSwitch = handleResizingBoundingBox( pos, planeId, primarySelectedEdge, @@ -587,6 +588,9 @@ export class BoundingBoxTool { }, leftMouseUp: () => { + if (primarySelectedEdge) { + Store.dispatch(finishedResizingUserBoundingBoxAction(primarySelectedEdge.boxId)); + } primarySelectedEdge = null; secondarySelectedEdge = null; getSceneController().highlightUserBoundingBox(null); diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index 65136ee3de0..f389306c541 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -6,7 +6,7 @@ import type { RemoteMeshMetaData, APIAnnotationVisibility, } from "types/api_flow_types"; -import type { Vector3 } from "oxalis/constants"; +import type { Vector3, BoundingBoxType } from "oxalis/constants"; import type { UserBoundingBox } from "oxalis/store"; type InitializeAnnotationAction = { @@ -39,6 +39,17 @@ type SetUserBoundingBoxesAction = { userBoundingBoxes: Array, }; +type SetUserBoundingBoxBoundsAction = { + type: "SET_USER_BOUNDING_BOX_BOUNDS", + bounds: BoundingBoxType, + id: number, +}; + +type FinishedResizingUserBoundingBoxAction = { + type: "FINISHED_RESIZING_USER_BOUNDING_BOX", + id: number, +}; + type AddUserBoundingBoxesAction = { type: "ADD_USER_BOUNDING_BOXES", userBoundingBoxes: Array, @@ -178,6 +189,8 @@ export type AnnotationActionTypes = | SetAnnotationAllowUpdateAction | UpdateRemoteMeshMetaDataAction | SetUserBoundingBoxesAction + | SetUserBoundingBoxBoundsAction + | FinishedResizingUserBoundingBoxAction | AddNewUserBoundingBox | SetUserBoundingBoxVisibilityAction | SetUserBoundingBoxNameAction @@ -204,6 +217,7 @@ export type AnnotationActionTypes = export type UserBoundingBoxAction = | SetUserBoundingBoxesAction + | SetUserBoundingBoxBoundsAction | AddNewUserBoundingBox | SetUserBoundingBoxVisibilityAction | SetUserBoundingBoxNameAction @@ -215,10 +229,12 @@ export const AllUserBoundingBoxActions = [ "SET_USER_BOUNDING_BOXES", "ADD_NEW_USER_BOUNDING_BOX", "SET_USER_BOUNDING_BOX_VISIBILITY", + "FINISHED_RESIZING_USER_BOUNDING_BOX", "SET_USER_BOUNDING_BOX_NAME", "SET_USER_BOUNDING_BOX_COLOR", "DELETE_USER_BOUNDING_BOX", "ADD_USER_BOUNDING_BOXES", + "SET_USER_BOUNDING_BOX_BOUNDS", ]; export const initializeAnnotationAction = ( @@ -263,6 +279,22 @@ export const setUserBoundingBoxesAction = ( userBoundingBoxes, }); +export const setUserBoundingBoxBoundsAction = ( + id: number, + bounds: BoundingBoxType, +): SetUserBoundingBoxBoundsAction => ({ + type: "SET_USER_BOUNDING_BOX_BOUNDS", + id, + bounds, +}); + +export const finishedResizingUserBoundingBoxAction = ( + id: number, +): FinishedResizingUserBoundingBoxAction => ({ + type: "FINISHED_RESIZING_USER_BOUNDING_BOX", + id, +}); + export const addUserBoundingBoxAction = ( newBoundingBox?: ?UserBoundingBox, center?: Vector3, diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index f33f4da9f5a..e51802bcc7a 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -75,6 +75,22 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, action.userBoundingBoxes); } + case "SET_USER_BOUNDING_BOX_BOUNDS": { + const tracing = maybeGetSomeTracing(state.tracing); + if (tracing == null) { + return state; + } + const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => + bbox.id === action.id + ? { + ...bbox, + boundingBox: action.bounds, + } + : bbox, + ); + return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + } + case "ADD_NEW_USER_BOUNDING_BOX": { const tracing = maybeGetSomeTracing(state.tracing); if (tracing == null) { diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index edf9b8b4db7..191b09f1fb1 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -86,6 +86,7 @@ import compressLz4Block from "oxalis/workers/byte_array_lz4_compression.worker"; import messages from "messages"; import { AllUserBoundingBoxActions, + setUserBoundingBoxesAction, type UserBoundingBoxAction, } from "oxalis/model/actions/annotation_actions"; import window, { alert, document, location } from "libs/window"; @@ -94,6 +95,10 @@ import { enforceSkeletonTracing } from "../accessors/skeletontracing_accessor"; const byteArrayToLz4Array = createWorker(compressLz4Block); +const UndoRedoRelevantBoundingBoxActions = AllUserBoundingBoxActions.filter( + action => action !== "SET_USER_BOUNDING_BOXES", +); + type UndoBucket = { zoomedBucketAddress: Vector4, data: Uint8Array, @@ -145,13 +150,14 @@ function unpackRelevantActionForUndo(action): RelevantActionsForUndoRedo { }; } + if (UndoRedoRelevantBoundingBoxActions.includes(action.type)) { + return { userBoundingBoxAction: ((action: any): UserBoundingBoxAction) }; + } + if (SkeletonTracingSaveRelevantActions.includes(action.type)) { return { skeletonUserAction: ((action: any): SkeletonTracingAction) }; } - if (AllUserBoundingBoxActions.includes(action.type)) { - return { userBoundingBoxAction: ((action: any): UserBoundingBoxAction) }; - } throw new Error("Could not unpack redux action from channel"); } @@ -163,7 +169,7 @@ const getUserBoundingBoxesFromState = state => { export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; - let previousAction: ?any = null; + let previousAction: ?Action = null; let prevSkeletonTracingOrNull: ?SkeletonTracing = null; let prevUserBoundingBoxes: Array = []; let pendingCompressions: Array> = []; @@ -174,7 +180,7 @@ export function* collectUndoStates(): Saga { prevUserBoundingBoxes = yield* select(getUserBoundingBoxesFromState); const actionChannel = yield _actionChannel([ ...SkeletonTracingSaveRelevantActions, - ...AllUserBoundingBoxActions, + ...UndoRedoRelevantBoundingBoxActions, "ADD_BUCKET_TO_UNDO", "FINISH_ANNOTATION_STROKE", "IMPORT_VOLUMETRACING", @@ -184,7 +190,6 @@ export function* collectUndoStates(): Saga { // TODO: Just trigger an action when dragging and when stopped dragging to be able to batch the actions while (true) { const currentAction = yield* take(actionChannel); - const { skeletonUserAction, addBucketToUndoAction, @@ -194,7 +199,6 @@ export function* collectUndoStates(): Saga { undo, redo, } = unpackRelevantActionForUndo(currentAction); - if ( skeletonUserAction || addBucketToUndoAction || @@ -233,17 +237,16 @@ export function* collectUndoStates(): Saga { currentVolumeAnnotationBatch = []; pendingCompressions = []; } else if (userBoundingBoxAction) { - const boundingBoxUndoState = yield* call( - getBoundingBoxToUndoState, + const boundingBoxUndoState = getBoundingBoxToUndoState( userBoundingBoxAction, - prevSkeletonTracingOrNull, + prevUserBoundingBoxes, previousAction, ); - if (skeletonUndoState) { + if (boundingBoxUndoState) { shouldClearRedoState = true; - undoStack.push(skeletonUndoState); + undoStack.push(boundingBoxUndoState); } - previousAction = skeletonUserAction; + previousAction = userBoundingBoxAction; } if (shouldClearRedoState) { // Clear the redo stack when a new action is executed. @@ -259,19 +262,29 @@ export function* collectUndoStates(): Saga { ({ type: "warning", reason: messages["undo.import_volume_tracing"] }: WarnUndoState), ); } else if (undo) { - if (undoStack.length > 0 && undoStack[undoStack.length - 1].type === "skeleton") { - previousAction = null; - } - yield* call(applyStateOfStack, undoStack, redoStack, prevSkeletonTracingOrNull, "undo"); + previousAction = null; + yield* call( + applyStateOfStack, + undoStack, + redoStack, + prevSkeletonTracingOrNull, + prevUserBoundingBoxes, + "undo", + ); if (undo.callback != null) { undo.callback(); } yield* put(setBusyBlockingInfoAction(false)); } else if (redo) { - if (redoStack.length > 0 && redoStack[redoStack.length - 1].type === "skeleton") { - previousAction = null; - } - yield* call(applyStateOfStack, redoStack, undoStack, prevSkeletonTracingOrNull, "redo"); + previousAction = null; + yield* call( + applyStateOfStack, + redoStack, + undoStack, + prevSkeletonTracingOrNull, + prevUserBoundingBoxes, + "redo", + ); if (redo.callback != null) { redo.callback(); } @@ -279,13 +292,14 @@ export function* collectUndoStates(): Saga { } // We need the updated tracing here prevSkeletonTracingOrNull = yield* select(state => state.tracing.skeleton); + prevUserBoundingBoxes = yield* select(getUserBoundingBoxesFromState); } } function* getSkeletonTracingToUndoState( skeletonUserAction: SkeletonTracingAction, prevTracing: SkeletonTracing, - previousAction: ?SkeletonTracingAction, + previousAction: ?Action, ): Saga { const curTracing = yield* select(state => enforceSkeletonTracing(state.tracing)); if (curTracing !== prevTracing) { @@ -296,13 +310,20 @@ function* getSkeletonTracingToUndoState( return null; } -function* getBoundingBoxToUndoState( +function getBoundingBoxToUndoState( userBoundingBoxAction: UserBoundingBoxAction, prevUserBoundingBoxes: Array, - previousAction: ?SkeletonTracingAction, -): Saga { - const currentUserBoundingBoxes = yield* select(getUserBoundingBoxesFromState); - if (shouldAddToUndoStack(userBoundingBoxAction, previousAction)) { + previousAction: ?Action, +): ?BoundingBoxUndoState { + const isSameActionOnSameBoundingBox = + previousAction != null && + userBoundingBoxAction.id != null && + previousAction.id != null && + userBoundingBoxAction.type === previousAction.type && + userBoundingBoxAction.id === previousAction.id; + const isFinishedResizingAction = + userBoundingBoxAction.type === "FINISHED_RESIZING_USER_BOUNDING_BOX"; + if (!isSameActionOnSameBoundingBox && !isFinishedResizingAction) { return { type: "bounding box", data: prevUserBoundingBoxes }; } return null; @@ -371,6 +392,7 @@ function* applyStateOfStack( sourceStack: Array, stackToPushTo: Array, prevSkeletonTracingOrNull: ?SkeletonTracing, + prevUserBoundingBoxes: ?Array, direction: "undo" | "redo", ): Saga { if (sourceStack.length <= 0) { @@ -420,6 +442,12 @@ function* applyStateOfStack( const currentVolumeState = yield* call(applyAndGetRevertingVolumeBatch, volumeBatchToApply); stackToPushTo.push(currentVolumeState); yield* call(progressCallback, true, `Finished ${direction}...`); + } else if (stateToRestore.type === "bounding box") { + if (prevUserBoundingBoxes != null) { + stackToPushTo.push({ type: "bounding box", data: prevUserBoundingBoxes }); + } + const newBoundingBoxes = stateToRestore.data; + yield* put(setUserBoundingBoxesAction(newBoundingBoxes)); } else if (stateToRestore.type === "warning") { Toast.info(stateToRestore.reason); } diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.js b/frontend/javascripts/oxalis/view/components/setting_input_views.js index 66cff6f8e92..5ea94775edd 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.js @@ -287,13 +287,6 @@ export function NumberInputPopoverSetting(props: NumberInputPopoverSettingProps) ); } -export type UserBoundingBoxInputUpdate = { - boundingBox?: Vector6, - name?: string, - color?: Vector3, - isVisible?: boolean, -}; - type UserBoundingBoxInputProps = { value: Vector6, name: string, @@ -301,7 +294,7 @@ type UserBoundingBoxInputProps = { isVisible: boolean, isExportEnabled: boolean, tooltipTitle: string, - onChange: UserBoundingBoxInputUpdate => void, + onBoundingChange: Vector6 => void, onDelete: () => void, onExport: () => void, onGoToBoundingBox: () => void, @@ -368,7 +361,7 @@ export class UserBoundingBoxInput extends React.PureComponent) => void, + setChangeBoundingBoxBounds: (number, BoundingBoxType) => void, addNewBoundingBox: () => void, deleteBoundingBox: number => void, setBoundingBoxVisibility: (number, boolean) => void, @@ -47,7 +44,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { const { tracing, dataset, - onChangeBoundingBoxes, + setChangeBoundingBoxBounds, addNewBoundingBox, setBoundingBoxVisibility, setBoundingBoxName, @@ -57,26 +54,8 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { } = props; const { userBoundingBoxes } = getSomeTracing(tracing); - function handleChangeUserBoundingBox( - id: number, - { boundingBox, name, color, isVisible }: UserBoundingBoxInputUpdate, - ) { - const maybeUpdatedBoundingBox = boundingBox - ? Utils.computeBoundingBoxFromArray(boundingBox) - : undefined; - - const updatedUserBoundingBoxes = userBoundingBoxes.map(bb => - bb.id === id - ? { - ...bb, - boundingBox: maybeUpdatedBoundingBox || bb.boundingBox, - name: name != null ? name : bb.name, - color: color || bb.color, - isVisible: isVisible != null ? isVisible : bb.isVisible, - } - : bb, - ); - onChangeBoundingBoxes(updatedUserBoundingBoxes); + function handleBoundingBoxBoundingChange(id: number, boundingBox: Vector6) { + setChangeBoundingBoxBounds(id, Utils.computeBoundingBoxFromArray(boundingBox)); } function handleBoundingBoxVisibilityChange(id: number, isVisible: boolean) { @@ -125,7 +104,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { name={bb.name} isExportEnabled={dataset.jobsEnabled} isVisible={bb.isVisible} - onChange={_.partial(handleChangeUserBoundingBox, bb.id)} + onBoundingChange={_.partial(handleBoundingBoxBoundingChange, bb.id)} onDelete={_.partial(handleDeleteUserBoundingBox, bb.id)} onExport={ dataset.jobsEnabled ? _.partial(setSelectedBoundingBoxForExport, bb) : () => {} @@ -168,8 +147,8 @@ const mapStateToProps = (state: OxalisState) => ({ }); const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ - onChangeBoundingBoxes(userBoundingBoxes: Array) { - dispatch(setUserBoundingBoxesAction(userBoundingBoxes)); + setChangeBoundingBoxBounds(id: number, bounds: BoundingBoxType) { + dispatch(setUserBoundingBoxBoundsAction(id, bounds)); }, addNewBoundingBox() { dispatch(addUserBoundingBoxAction()); From 7c3ce6d6d32ad2dcfb848fa3e6541e66b77079aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 22 Oct 2021 14:11:55 +0200 Subject: [PATCH 22/34] fix flow --- frontend/javascripts/oxalis/model/sagas/save_saga.js | 3 ++- frontend/javascripts/oxalis/view/context_menu.js | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 191b09f1fb1..90ef2e8d1fe 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -169,7 +169,8 @@ const getUserBoundingBoxesFromState = state => { export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; - let previousAction: ?Action = null; + // This variable must be any (no Action) as otherwise cyclic dependencies are created which flow cannot handle. + let previousAction: ?any = null; let prevSkeletonTracingOrNull: ?SkeletonTracing = null; let prevUserBoundingBoxes: Array = []; let pendingCompressions: Array> = []; diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index d420d49cb6a..15a45e48ff1 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -533,8 +533,6 @@ function NoNodeContextMenuOptions({ return null; } - // const me - return ( { From a4598ed6b2e49c8dd39bd4afed0c6f4f48dfcac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Fri, 22 Oct 2021 15:52:04 +0200 Subject: [PATCH 23/34] refactor code --- .../combinations/bounding_box_handlers.js | 40 +++++++++- .../controller/combinations/tool_controls.js | 79 +++++++++---------- .../oxalis/controller/scene_controller.js | 1 - .../javascripts/oxalis/geometries/cube.js | 7 +- .../oxalis/model/reducers/reducer_helpers.js | 4 +- .../oxalis/model/sagas/save_saga.js | 2 +- .../javascripts/oxalis/view/input_catcher.js | 2 +- 7 files changed, 79 insertions(+), 56 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index e5c09ddd514..c72d9f098ca 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -7,6 +7,9 @@ import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import Dimension from "oxalis/model/dimensions"; import { setUserBoundingBoxBoundsAction } from "oxalis/model/actions/annotation_actions"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; +import getSceneController from "oxalis/controller/scene_controller_provider"; + +const BOUNDING_BOX_HOVERING_THROTTLE_TIME = 100; const getNeighbourEdgeIndexByEdgeIndex = { // TODO: Use this to detect corners properly. @@ -34,9 +37,9 @@ function getDistanceToBoundingBoxEdge( otherDim: number, planeRatio: Vector3, ) { - // There are four cases how the distance to an edge needs to be calculated. - // Here are all cases visualized via a number that are referenced below: - // Note that this is the perspective of the rendered bounding box cross section. + // There are three cases how the distance to an edge needs to be calculated. + // Here are all cases visualized via numbers that are referenced below: + // Note that this is the perspective of the rendered bounding box cross section in a viewport. // ---> x // | 1 1 // ↓ '. .' @@ -50,7 +53,7 @@ function getDistanceToBoundingBoxEdge( // .' '. // 2 2 // - // This example is for the xy viewport for x as the main direction / edgeDim. + // This example is for the xy viewport for y as the main direction / edgeDim. // As the planeRatio is multiplied to the global coordinates passed to this method, // the distance between the mouse and the bounding box is distorted by the factor of planeRatio. @@ -195,6 +198,35 @@ export function getClosestHoveredBoundingBox( return [primaryEdge, secondaryEdge]; } +export const highlightAndSetCursorOnHoveredBoundingBox = _.throttle( + (delta: Point2, position: Point2, planeId: OrthoView) => { + const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, planeId); + const inputCatcher = document.getElementById(`inputcatcher_${planeId}`); + if (hoveredEdgesInfo != null && inputCatcher != null) { + const [primaryHoveredEdge, secondaryHoveredEdge] = hoveredEdgesInfo; + getSceneController().highlightUserBoundingBox(primaryHoveredEdge.boxId); + if (secondaryHoveredEdge != null) { + // If a corner is selected. + inputCatcher.style.cursor = + (primaryHoveredEdge.isMaxEdge && secondaryHoveredEdge.isMaxEdge) || + (!primaryHoveredEdge.isMaxEdge && !secondaryHoveredEdge.isMaxEdge) + ? "nwse-resize" + : "nesw-resize"; + } else if (primaryHoveredEdge.direction === "horizontal") { + inputCatcher.style.cursor = "row-resize"; + } else { + inputCatcher.style.cursor = "col-resize"; + } + } else { + getSceneController().highlightUserBoundingBox(null); + if (inputCatcher != null) { + inputCatcher.style.cursor = "auto"; + } + } + }, + BOUNDING_BOX_HOVERING_THROTTLE_TIME, +); + export function handleResizingBoundingBox( mousePosition: Point2, planeId: OrthoView, diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index fb0c988e4bc..ad1fa498fe3 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -1,6 +1,5 @@ // @flow import { type ModifierKeys } from "libs/input"; -import _ from "lodash"; import { type OrthoView, OrthoViews, @@ -9,6 +8,7 @@ import { type ShowContextMenuFunction, type AnnotationTool, AnnotationToolEnum, + OrthoViewValuesWithoutTDView, } from "oxalis/constants"; import { getContourTracingMode, @@ -26,6 +26,7 @@ import { type SelectedEdge, getClosestHoveredBoundingBox, handleResizingBoundingBox, + highlightAndSetCursorOnHoveredBoundingBox, } from "oxalis/controller/combinations/bounding_box_handlers"; import Store from "oxalis/store"; import * as Utils from "libs/utils"; @@ -145,6 +146,8 @@ export class MoveTool { rightClick: "Context Menu", }; } + + static onToolDeselected() {} } export class SkeletonTool { @@ -294,6 +297,8 @@ export class SkeletonTool { rightClick: useLegacyBindings && !shiftKey ? "Place Node" : "Context Menu", }; } + + static onToolDeselected() {} } export class DrawTool { @@ -415,6 +420,8 @@ export class DrawTool { rightClick, }; } + + static onToolDeselected() {} } export class EraseTool { @@ -465,6 +472,8 @@ export class EraseTool { rightClick: "Context Menu", }; } + + static onToolDeselected() {} } export class PickCellTool { @@ -488,6 +497,8 @@ export class PickCellTool { rightClick: "Context Menu", }; } + + static onToolDeselected() {} } export class FillCellTool { @@ -520,6 +531,8 @@ export class FillCellTool { rightClick: "Context Menu", }; } + + static onToolDeselected() {} } export class BoundingBoxTool { @@ -528,38 +541,8 @@ export class BoundingBoxTool { planeView: PlaneView, showNodeContextMenuAt: ShowContextMenuFunction, ): * { - const bboxHoveringThrottleTime = 100; let primarySelectedEdge: ?SelectedEdge = null; let secondarySelectedEdge: ?SelectedEdge = null; - const getClosestHoveredBoundingBoxThrottled = - planeId !== OrthoViews.TDView - ? _.throttle((delta: Point2, position: Point2) => { - const { body } = document; - if (body == null || primarySelectedEdge != null) { - return; - } - const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, planeId); - if (hoveredEdgesInfo != null) { - const [primaryHoveredEdge, secondaryHoveredEdge] = hoveredEdgesInfo; - getSceneController().highlightUserBoundingBox(primaryHoveredEdge.boxId); - if (secondaryHoveredEdge != null) { - // If a corner is selected. - body.style.cursor = - (primaryHoveredEdge.isMaxEdge && secondaryHoveredEdge.isMaxEdge) || - (!primaryHoveredEdge.isMaxEdge && !secondaryHoveredEdge.isMaxEdge) - ? "nwse-resize" - : "nesw-resize"; - } else if (primaryHoveredEdge.direction === "horizontal") { - body.style.cursor = "row-resize"; - } else { - body.style.cursor = "col-resize"; - } - } else { - getSceneController().highlightUserBoundingBox(null); - body.style.cursor = "auto"; - } - }, bboxHoveringThrottleTime) - : (_delta: Point2, _position: Point2) => {}; return { leftDownMove: (delta: Point2, pos: Point2, _id: ?string, _event: MouseEvent) => { if (primarySelectedEdge != null) { @@ -586,6 +569,7 @@ export class BoundingBoxTool { getSceneController().highlightUserBoundingBox(primarySelectedEdge.boxId); } }, + // TODO: Add unselect tool mechanism leftMouseUp: () => { if (primarySelectedEdge) { @@ -598,7 +582,9 @@ export class BoundingBoxTool { mouseMove: (delta: Point2, position: Point2, _id, event: MouseEvent) => { MoveHandlers.moveWhenAltIsPressed(delta, position, _id, event); - getClosestHoveredBoundingBoxThrottled(delta, position); + if (primarySelectedEdge == null && planeId !== OrthoViews.TDView) { + highlightAndSetCursorOnHoveredBoundingBox(delta, position, planeId); + } }, rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { @@ -615,24 +601,31 @@ export class BoundingBoxTool { } static getActionDescriptors( - activeTool: AnnotationTool, - useLegacyBindings: boolean, + _activeTool: AnnotationTool, + _useLegacyBindings: boolean, _shiftKey: boolean, _ctrlKey: boolean, _altKey: boolean, ): Object { - let rightClick; - if (!useLegacyBindings) { - rightClick = "Context Menu"; - } else { - rightClick = `Erase (${activeTool === AnnotationToolEnum.BRUSH ? "Brush" : "Trace"})`; - } - return { - leftDrag: activeTool === AnnotationToolEnum.BRUSH ? "Brush" : "Trace", - rightClick, + leftDrag: "Resize Bounding Boxes", + rightClick: "Context Menu", }; } + + static onToolDeselected() { + const { body } = document; + if (body == null) { + return; + } + for (const planeId of OrthoViewValuesWithoutTDView) { + const inputCatcher = document.getElementById(`inputcatcher_${planeId}`); + if (inputCatcher) { + inputCatcher.style.cursor = "auto"; + } + } + getSceneController().highlightUserBoundingBox(null); + } } const toolToToolClass = { diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 7296b196def..2203b6b42c3 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -462,7 +462,6 @@ class SceneController { color: Utils.rgbToInt(bbColor), showCrossSections: true, id, - isEditable: true, isHighlighted: this.highlightedBBoxId === id, }); bbCube.setVisibility(isVisible); diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index 99f328048ea..b21e7b9ef41 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -26,7 +26,6 @@ type Properties = { color?: number, showCrossSections?: boolean, id?: number, - isEditable?: boolean, isHighlighted: boolean, }; @@ -39,21 +38,19 @@ class Cube { showCrossSections: boolean; initialized: boolean; visible: boolean; - id: ?number; - isEditable: boolean; lineWidth: number; color: number; + id: ?number; isHighlighted: boolean; constructor(properties: Properties) { // min/max should denote a half-open interval. this.min = properties.min || [0, 0, 0]; this.max = properties.max; - this.id = properties.id; - this.isEditable = properties.isEditable || false; this.lineWidth = properties.lineWidth != null ? properties.lineWidth : 1; this.color = properties.color || 0x000000; this.showCrossSections = properties.showCrossSections || false; + this.id = properties.id; this.initialized = false; this.visible = true; diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js index 309fa5b8aa2..09b381e8767 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -18,6 +18,7 @@ import type { Boundary } from "oxalis/model/accessors/dataset_accessor"; import type { BoundingBoxType, AnnotationTool } from "oxalis/constants"; import { V3 } from "libs/mjs"; import * as Utils from "libs/utils"; +import { getToolClassForAnnotationTool } from "oxalis/controller/combinations/tool_controls"; import { isVolumeTool, isVolumeAnnotationDisallowedForZoom, @@ -142,6 +143,7 @@ export function setToolReducer(state: OxalisState, tool: AnnotationTool) { if (isVolumeTool(tool) && isVolumeAnnotationDisallowedForZoom(tool, state)) { return state; } - + // Execute deselection event. + getToolClassForAnnotationTool(state.uiInformation.activeTool).onToolDeselected(); return updateKey(state, "uiInformation", { activeTool: tool }); } diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 90ef2e8d1fe..a6ec1b1f674 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -188,7 +188,7 @@ export function* collectUndoStates(): Saga { "UNDO", "REDO", ]); - // TODO: Just trigger an action when dragging and when stopped dragging to be able to batch the actions + while (true) { const currentAction = yield* take(actionChannel); const { diff --git a/frontend/javascripts/oxalis/view/input_catcher.js b/frontend/javascripts/oxalis/view/input_catcher.js index 36657deac8e..a1c4083a0ae 100644 --- a/frontend/javascripts/oxalis/view/input_catcher.js +++ b/frontend/javascripts/oxalis/view/input_catcher.js @@ -109,7 +109,7 @@ class InputCatcher extends React.PureComponent { className={`inputcatcher ${viewportID}`} style={{ position: "relative", - cursor: this.props.busyBlockingInfo.isBusy ? "wait" : "inherit", + cursor: this.props.busyBlockingInfo.isBusy ? "wait" : "auto", }} > From 5514ba1a1f86f319fe4a0191deac4662815657f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 25 Oct 2021 11:54:49 +0200 Subject: [PATCH 24/34] fix tests --- frontend/javascripts/libs/window.js | 2 +- .../oxalis/controller/combinations/bounding_box_handlers.js | 1 + .../javascripts/oxalis/controller/combinations/tool_controls.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/libs/window.js b/frontend/javascripts/libs/window.js index fa92f1bca28..c60adfc15d8 100644 --- a/frontend/javascripts/libs/window.js +++ b/frontend/javascripts/libs/window.js @@ -4,7 +4,7 @@ export const alert = typeof window === "undefined" ? console.log.bind(console) : window.alert; export const document = typeof window === "undefined" || !window.document - ? { getElementById: () => null } + ? { getElementById: () => null, body: null } : window.document; // See https://github.com/facebook/flow/blob/master/lib/bom.js#L294-L311 diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index c72d9f098ca..f009b63e9e5 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -8,6 +8,7 @@ import Dimension from "oxalis/model/dimensions"; import { setUserBoundingBoxBoundsAction } from "oxalis/model/actions/annotation_actions"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import getSceneController from "oxalis/controller/scene_controller_provider"; +import { document } from "libs/window"; const BOUNDING_BOX_HOVERING_THROTTLE_TIME = 100; diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index ad1fa498fe3..03d58ae4545 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -31,6 +31,7 @@ import { import Store from "oxalis/store"; import * as Utils from "libs/utils"; import * as VolumeHandlers from "oxalis/controller/combinations/volume_handlers"; +import { document } from "libs/window"; import api from "oxalis/api/internal_api"; /* From da41fe6eb2008b50118f9391a098ef553135e9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 1 Nov 2021 16:54:19 +0100 Subject: [PATCH 25/34] Apply a lot of feedback --- .../combinations/bounding_box_handlers.js | 182 ++++++------ .../controller/combinations/tool_controls.js | 14 +- .../oxalis/controller/scene_controller.js | 8 +- .../javascripts/oxalis/geometries/cube.js | 1 - .../model/accessors/tracing_accessor.js | 11 +- .../model/accessors/view_mode_accessor.js | 32 +-- .../model/actions/annotation_actions.js | 89 ++---- .../oxalis/model/actions/ui_actions.js | 4 +- .../model/reducers/annotation_reducer.js | 76 ++--- .../oxalis/model/reducers/reducer_helpers.js | 24 +- .../oxalis/model/reducers/ui_reducer.js | 27 +- .../model/sagas/annotation_tool_saga.js | 23 ++ .../oxalis/model/sagas/root_saga.js | 2 + .../oxalis/model/sagas/save_saga.js | 9 +- .../oxalis/model/sagas/volumetracing_saga.js | 2 - frontend/javascripts/oxalis/store.js | 16 +- .../oxalis/view/action-bar/toolbar_view.js | 12 +- .../javascripts/oxalis/view/context_menu.js | 268 +++++++++--------- .../view/layouting/tracing_layout_view.js | 16 +- .../right-border-tabs/bounding_box_tab.js | 114 ++------ .../volumetracing_saga_integration.spec.js | 3 +- 21 files changed, 396 insertions(+), 537 deletions(-) create mode 100644 frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index f009b63e9e5..50c9004e09a 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -1,11 +1,11 @@ // @flow import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor"; import _ from "lodash"; -import { type OrthoView, type Point2, type Vector3 } from "oxalis/constants"; +import type { OrthoView, Point2, Vector3, BoundingBoxType } from "oxalis/constants"; import Store from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; -import Dimension from "oxalis/model/dimensions"; -import { setUserBoundingBoxBoundsAction } from "oxalis/model/actions/annotation_actions"; +import Dimension, { type DimensionMap, type DimensionIndices } from "oxalis/model/dimensions"; +import { changeUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import getSceneController from "oxalis/controller/scene_controller_provider"; import { document } from "libs/window"; @@ -13,7 +13,6 @@ import { document } from "libs/window"; const BOUNDING_BOX_HOVERING_THROTTLE_TIME = 100; const getNeighbourEdgeIndexByEdgeIndex = { - // TODO: Use this to detect corners properly. // The edges are indexed within the plane like this: // See the distanceArray calculation as a reference. // +---0---+ @@ -27,6 +26,7 @@ const getNeighbourEdgeIndexByEdgeIndex = { "2": [0, 1], "3": [0, 1], }; +// This value is in "mouse tolerance to trigger a selection". It is in "unzoomed worldcoordinates". const MAX_DISTANCE_TO_SELECTION = 15; function getDistanceToBoundingBoxEdge( @@ -34,55 +34,64 @@ function getDistanceToBoundingBoxEdge( min: Vector3, max: Vector3, compareToMin: boolean, - edgeDim: number, - otherDim: number, + primaryAndSecondaryDim: [DimensionIndices, DimensionIndices], planeRatio: Vector3, ) { - // There are three cases how the distance to an edge needs to be calculated. - // Here are all cases visualized via numbers that are referenced below: - // Note that this is the perspective of the rendered bounding box cross section in a viewport. + // This method calculates the distance between the given pos and a single edge of a cross section + // of bounding box given min and max that is displayed in a certain viewport. + // The edge dimension that the edge extents along is given by the first entry of primaryAndSecondaryDim. + // Here goes 0 = x direction, 1 = y direction and 2 = z direction. + // The second entry of primaryAndSecondaryDim gives the other extend of the viewport / cross section. + // The boolean compareToMin tells which of the two edges in the primary direction should be compared with. + // To calculate the distance there are three cases depending on the primary dimension value of pos. + // - One when the given pos is on the left of the minimum of the edge (case 1), + // - one if pos is on the right of the maximum of the edge (case 2) + // - and the last if pos is inbeween the minimum and maximum of the edge (case 3). + // If the cross section is on the xy viewport and the edge to compare to is the min edge in x direction + // an example for the different cases is: compareToMin = true, primaryAndSecondaryDim = [0,1] = [x,y] // ---> x - // | 1 1 - // ↓ '. .' - // y ↘ ↙ - // +-------+ - // | | - // 3 --> | | <-- 3 - // | | - // +-------+ - // ↗ ↖ - // .' '. - // 2 2 - // - // This example is for the xy viewport for y as the main direction / edgeDim. + // | min.x max.x + // ↓ y ↓ ↓ + // ↓ ↓ + // case 1 ↓ case 3 case 3 ↓ case 2 + // '. | | .' + // '↘ ↓ ↓ ↙' + // -------------------------------------------------------- + // | | + // | edge extends along x direction -> primary dim = x | + // | | // As the planeRatio is multiplied to the global coordinates passed to this method, // the distance between the mouse and the bounding box is distorted by the factor of planeRatio. // That's why we later divide exactly by this factor to let the hit box / distance // between the mouse and bounding box be the same in each dimension. + const [primaryEdgeDim, secondaryprimaryEdgeDim] = primaryAndSecondaryDim; const cornerToCompareWith = compareToMin ? min : max; - if (pos[edgeDim] < min[edgeDim]) { - // Case 1: Distance to the min corner is needed in edgeDim. + if (pos[primaryEdgeDim] < min[primaryEdgeDim]) { + // Case 1: Distance to the min corner is needed in primaryEdgeDim. return ( - Math.sqrt( - Math.abs(pos[edgeDim] - min[edgeDim]) ** 2 + - Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, - ) / planeRatio[edgeDim] + Math.hypot( + pos[primaryEdgeDim] - min[primaryEdgeDim], + pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim], + ) / planeRatio[primaryEdgeDim] ); } - if (pos[edgeDim] > max[edgeDim]) { - // Case 2: Distance to max Corner is needed in edgeDim. + if (pos[primaryEdgeDim] > max[primaryEdgeDim]) { + // Case 2: Distance to max Corner is needed in primaryEdgeDim. return ( - Math.sqrt( - Math.abs(pos[edgeDim] - max[edgeDim]) ** 2 + - Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2, - ) / planeRatio[edgeDim] + Math.hypot( + pos[primaryEdgeDim] - max[primaryEdgeDim], + pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim], + ) / planeRatio[primaryEdgeDim] ); } // Case 3: - // If the position is within the bounds of the edgeDim, the shortest distance - // to the edge is simply the difference between the otherDim values. - return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) / planeRatio[edgeDim]; + // If the position is within the bounds of the primaryEdgeDim, the shortest distance + // to the edge is simply the difference between the secondaryprimaryEdgeDim values. + return ( + Math.abs(pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim]) / + planeRatio[primaryEdgeDim] + ); } export type SelectedEdge = { @@ -93,6 +102,37 @@ export type SelectedEdge = { resizableDimension: 0 | 1 | 2, }; +type DistanceArray = [number, number, number, number]; + +function computeDistanceArray( + boundingBoxBounds: BoundingBoxType, + globalPosition: Vector3, + indices: DimensionMap, + planeRatio: Vector3, +): DistanceArray { + const { min, max } = boundingBoxBounds; + const distanceArray = [0, 1, 2, 3].map(edgeId => { + const direction = edgeId < 2 ? "horizontal" : "vertical"; + const isMaxEdge = edgeId % 2 === 1; + const primaryAndSecondaryDim = + direction === "horizontal" ? [indices[0], indices[1]] : [indices[1], indices[0]]; + return getDistanceToBoundingBoxEdge( + globalPosition, + min, + max, + !isMaxEdge, + primaryAndSecondaryDim, + planeRatio, + ); + }); + return ((distanceArray: any): DistanceArray); +} + +// Return the edge or edges of the bounding box closest to the mouse position if their distance is below a certain threshold. +// If no edge is close to the mouse null is returned instead. Otherwise the first entry is always the closest edge. +// If the mouse near a corner, there is always an additional edge that is close to the mouse. +// If such an edge exists then this edge is the second entry of the array. +// If the mouse isn't close to a corner of a crossection, the second entry is null. export function getClosestHoveredBoundingBox( pos: Point2, plane: OrthoView, @@ -100,9 +140,9 @@ export function getClosestHoveredBoundingBox( const state = Store.getState(); const globalPosition = calculateGlobalPos(state, pos, plane); const { userBoundingBoxes } = getSomeTracing(state.tracing); - const reorderedIndices = Dimension.getIndices(plane); + const indices = Dimension.getIndices(plane); const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); - const thirdDim = reorderedIndices[2]; + const thirdDim = indices[2]; const zoomedMaxDistanceToSelection = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep; let currentNearestDistance = zoomedMaxDistanceToSelection; @@ -118,44 +158,12 @@ export function getClosestHoveredBoundingBox( } // In getNeighbourEdgeIndexByEdgeIndex is a visualization // of how the indices of the array map to the visible bbox edges. - const distanceArray = [ - getDistanceToBoundingBoxEdge( - globalPosition, - min, - max, - true, - reorderedIndices[0], - reorderedIndices[1], - planeRatio, - ), - getDistanceToBoundingBoxEdge( - globalPosition, - min, - max, - false, - reorderedIndices[0], - reorderedIndices[1], - planeRatio, - ), - getDistanceToBoundingBoxEdge( - globalPosition, - min, - max, - true, - reorderedIndices[1], - reorderedIndices[0], - planeRatio, - ), - getDistanceToBoundingBoxEdge( - globalPosition, - min, - max, - false, - reorderedIndices[1], - reorderedIndices[0], - planeRatio, - ), - ]; + const distanceArray = computeDistanceArray( + bbox.boundingBox, + globalPosition, + indices, + planeRatio, + ); const minimumDistance = Math.min(...distanceArray); if (minimumDistance < currentNearestDistance) { currentNearestDistance = minimumDistance; @@ -170,7 +178,7 @@ export function getClosestHoveredBoundingBox( const getEdgeInfoFromId = (edgeId: number) => { const direction = edgeId < 2 ? "horizontal" : "vertical"; const isMaxEdge = edgeId % 2 === 1; - const resizableDimension = edgeId < 2 ? reorderedIndices[1] : reorderedIndices[0]; + const resizableDimension = direction === "horizontal" ? indices[1] : indices[0]; return { boxId: nearestBoundingBox.id, direction, @@ -233,14 +241,13 @@ export function handleResizingBoundingBox( planeId: OrthoView, primaryEdge: SelectedEdge, secondaryEdge: ?SelectedEdge, -): { primary: boolean, secondary: boolean } { +) { const state = Store.getState(); const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId); const { userBoundingBoxes } = getSomeTracing(state.tracing); - const didMinAndMaxSwitch = { primary: false, secondary: false }; const bboxToResize = userBoundingBoxes.find(bbox => bbox.id === primaryEdge.boxId); if (!bboxToResize) { - return didMinAndMaxSwitch; + return; } const updatedBounds = { min: [...bboxToResize.boundingBox.min], @@ -271,10 +278,15 @@ export function handleResizingBoundingBox( return false; } } - didMinAndMaxSwitch.primary = updateBoundsAccordingToEdge(primaryEdge); + let didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge(primaryEdge); + if (didMinAndMaxEdgeSwitch) { + primaryEdge.isMaxEdge = !primaryEdge.isMaxEdge; + } if (secondaryEdge) { - didMinAndMaxSwitch.secondary = updateBoundsAccordingToEdge(secondaryEdge); + didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge(secondaryEdge); + if (didMinAndMaxEdgeSwitch) { + secondaryEdge.isMaxEdge = !secondaryEdge.isMaxEdge; + } } - Store.dispatch(setUserBoundingBoxBoundsAction(primaryEdge.boxId, updatedBounds)); - return didMinAndMaxSwitch; + Store.dispatch(changeUserBoundingBoxAction(primaryEdge.boxId, { boundingBox: updatedBounds })); } diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 03d58ae4545..61971394bf5 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -547,18 +547,7 @@ export class BoundingBoxTool { return { leftDownMove: (delta: Point2, pos: Point2, _id: ?string, _event: MouseEvent) => { if (primarySelectedEdge != null) { - const didMinAndMaxSwitch = handleResizingBoundingBox( - pos, - planeId, - primarySelectedEdge, - secondarySelectedEdge, - ); - if (didMinAndMaxSwitch.primary) { - primarySelectedEdge.isMaxEdge = !primarySelectedEdge.isMaxEdge; - } - if (didMinAndMaxSwitch.secondary && secondarySelectedEdge) { - secondarySelectedEdge.isMaxEdge = !secondarySelectedEdge.isMaxEdge; - } + handleResizingBoundingBox(pos, planeId, primarySelectedEdge, secondarySelectedEdge); } else { MoveHandlers.handleMovePlane(delta); } @@ -570,7 +559,6 @@ export class BoundingBoxTool { getSceneController().highlightUserBoundingBox(primarySelectedEdge.boxId); } }, - // TODO: Add unselect tool mechanism leftMouseUp: () => { if (primarySelectedEdge) { diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 2203b6b42c3..d3e349b2f38 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -477,17 +477,17 @@ class SceneController { if (this.highlightedBBoxId === bboxId) { return; } - const highlightOrUnhighlightUserBBox = (id: number, highlight: boolean) => { + const setIsHighlighted = (id: number, isHighlighted: boolean) => { const bboxToChangeHighlighting = this.userBoundingBoxes.find(bbCube => bbCube.id === id); if (bboxToChangeHighlighting != null) { - bboxToChangeHighlighting.setIsHighlighted(highlight); + bboxToChangeHighlighting.setIsHighlighted(isHighlighted); } }; if (this.highlightedBBoxId != null) { - highlightOrUnhighlightUserBBox(this.highlightedBBoxId, false); + setIsHighlighted(this.highlightedBBoxId, false); } if (bboxId != null) { - highlightOrUnhighlightUserBBox(bboxId, true); + setIsHighlighted(bboxId, true); } this.highlightedBBoxId = bboxId; } diff --git a/frontend/javascripts/oxalis/geometries/cube.js b/frontend/javascripts/oxalis/geometries/cube.js index b21e7b9ef41..cfa94f8ea20 100644 --- a/frontend/javascripts/oxalis/geometries/cube.js +++ b/frontend/javascripts/oxalis/geometries/cube.js @@ -146,7 +146,6 @@ class Cube { if (!this.initialized) { return; } - // Why not generally avoid updating when the position for the third dim is outside the bbox? for (const planeId of OrthoViewValuesWithoutTDView) { const thirdDim = dimensions.thirdDimensionForPlane(planeId); const { geometry } = this.crossSections[planeId]; diff --git a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js index a93122f9a0d..ef5caefbac8 100644 --- a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js @@ -25,14 +25,11 @@ export function maybeGetSomeTracing( export function getSomeTracing( tracing: Tracing, ): SkeletonTracing | VolumeTracing | ReadOnlyTracing { - if (tracing.skeleton != null) { - return tracing.skeleton; - } else if (tracing.volume != null) { - return tracing.volume; - } else if (tracing.readOnly != null) { - return tracing.readOnly; + const maybeSomeTracing = maybeGetSomeTracing(tracing); + if (maybeSomeTracing == null) { + throw new Error("The active annotation does not contain skeletons nor volume data"); } - throw new Error("The active annotation does not contain skeletons nor volume data"); + return maybeSomeTracing; } export function getSomeServerTracing( diff --git a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js index e0ed1b3ff4e..8997c29410f 100644 --- a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.js @@ -1,7 +1,7 @@ // @flow import memoizeOne from "memoize-one"; - +import _ from "lodash"; import { type OxalisState } from "oxalis/store"; import constants, { ArbitraryViewport, @@ -13,6 +13,7 @@ import constants, { type OrthoView, type Point2, type Vector3, + OrthoViewValuesWithoutTDView, type ViewMode, } from "oxalis/constants"; import { V3 } from "libs/mjs"; @@ -147,34 +148,17 @@ function _calculateGlobalPos(state: OxalisState, clickPos: Point2, planeId: ?Ort export function getDisplayedDataExtentInPlaneMode(state: OxalisState) { const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale); const curGlobalCenterPos = getPosition(state.flycam); - const xyExtent = getPlaneExtentInVoxelFromStore( - state, - state.flycam.zoomStep, - OrthoViews.PLANE_XY, - ); - const yzExtent = getPlaneExtentInVoxelFromStore( - state, - state.flycam.zoomStep, - OrthoViews.PLANE_YZ, - ); - const xzExtent = getPlaneExtentInVoxelFromStore( - state, - state.flycam.zoomStep, - OrthoViews.PLANE_XZ, + const extents = OrthoViewValuesWithoutTDView.map(orthoView => + getPlaneExtentInVoxelFromStore(state, state.flycam.zoomStep, orthoView), ); + const [xyExtent, yzExtent, xzExtent] = extents; const minExtent = 1; - const getMinExtent = (val1, val2) => { - if (val1 <= 0) { - return Math.max(val2, minExtent); - } - if (val2 <= 0) { - return Math.max(val1, minExtent); - } - return val1 < val2 ? val1 : val2; - }; + const getMinExtent = (val1, val2) => _.min([val1, val2].filter(v => v >= minExtent)) || minExtent; const xMinExtent = getMinExtent(xyExtent[0], xzExtent[0]) * planeRatio[0]; const yMinExtent = getMinExtent(xyExtent[1], yzExtent[1]) * planeRatio[1]; const zMinExtent = getMinExtent(xzExtent[1], yzExtent[0]) * planeRatio[2]; + // The new bounding box should cover half of what is displayed in the viewports. + // As the flycam position is taken as a center, the factor is halved again, resulting in a 0.25. const extentFactor = 0.25; const halfBoxExtent = [ Math.max(xMinExtent * extentFactor, 1), diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index f389306c541..fd854835d2e 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -6,8 +6,12 @@ import type { RemoteMeshMetaData, APIAnnotationVisibility, } from "types/api_flow_types"; -import type { Vector3, BoundingBoxType } from "oxalis/constants"; -import type { UserBoundingBox } from "oxalis/store"; +import type { Vector3 } from "oxalis/constants"; +import type { + UserBoundingBox, + UserBoundingBoxWithoutId, + UserBoundingBoxWithoutIdMaybe, +} from "oxalis/store"; type InitializeAnnotationAction = { type: "INITIALIZE_ANNOTATION", @@ -39,12 +43,6 @@ type SetUserBoundingBoxesAction = { userBoundingBoxes: Array, }; -type SetUserBoundingBoxBoundsAction = { - type: "SET_USER_BOUNDING_BOX_BOUNDS", - bounds: BoundingBoxType, - id: number, -}; - type FinishedResizingUserBoundingBoxAction = { type: "FINISHED_RESIZING_USER_BOUNDING_BOX", id: number, @@ -57,26 +55,16 @@ type AddUserBoundingBoxesAction = { type AddNewUserBoundingBox = { type: "ADD_NEW_USER_BOUNDING_BOX", - newBoundingBox?: ?UserBoundingBox, + newBoundingBox?: ?UserBoundingBoxWithoutId, + // Center is the passed position that the new bounding box should have as a center. + // If no center is given, the flycam center will be taken. center?: Vector3, }; -type SetUserBoundingBoxVisibilityAction = { - type: "SET_USER_BOUNDING_BOX_VISIBILITY", +type ChangeUserBoundingBoxAction = { + type: "CHANGE_USER_BOUNDING_BOX", id: number, - isVisible: boolean, -}; - -type SetUserBoundingBoxNameAction = { - type: "SET_USER_BOUNDING_BOX_NAME", - id: number, - name: string, -}; - -type SetUserBoundingBoxColorAction = { - type: "SET_USER_BOUNDING_BOX_COLOR", - id: number, - color: Vector3, + newProps: UserBoundingBoxWithoutIdMaybe, }; type DeleteUserBoundingBox = { @@ -189,12 +177,9 @@ export type AnnotationActionTypes = | SetAnnotationAllowUpdateAction | UpdateRemoteMeshMetaDataAction | SetUserBoundingBoxesAction - | SetUserBoundingBoxBoundsAction + | ChangeUserBoundingBoxAction | FinishedResizingUserBoundingBoxAction | AddNewUserBoundingBox - | SetUserBoundingBoxVisibilityAction - | SetUserBoundingBoxNameAction - | SetUserBoundingBoxColorAction | DeleteUserBoundingBox | AddUserBoundingBoxesAction | AddMeshMetadataAction @@ -217,24 +202,17 @@ export type AnnotationActionTypes = export type UserBoundingBoxAction = | SetUserBoundingBoxesAction - | SetUserBoundingBoxBoundsAction | AddNewUserBoundingBox - | SetUserBoundingBoxVisibilityAction - | SetUserBoundingBoxNameAction - | SetUserBoundingBoxColorAction | DeleteUserBoundingBox | AddUserBoundingBoxesAction; export const AllUserBoundingBoxActions = [ "SET_USER_BOUNDING_BOXES", "ADD_NEW_USER_BOUNDING_BOX", - "SET_USER_BOUNDING_BOX_VISIBILITY", + "CHANGE_USER_BOUNDING_BOX", "FINISHED_RESIZING_USER_BOUNDING_BOX", - "SET_USER_BOUNDING_BOX_NAME", - "SET_USER_BOUNDING_BOX_COLOR", "DELETE_USER_BOUNDING_BOX", "ADD_USER_BOUNDING_BOXES", - "SET_USER_BOUNDING_BOX_BOUNDS", ]; export const initializeAnnotationAction = ( @@ -279,13 +257,13 @@ export const setUserBoundingBoxesAction = ( userBoundingBoxes, }); -export const setUserBoundingBoxBoundsAction = ( +export const changeUserBoundingBoxAction = ( id: number, - bounds: BoundingBoxType, -): SetUserBoundingBoxBoundsAction => ({ - type: "SET_USER_BOUNDING_BOX_BOUNDS", + newProps: UserBoundingBoxWithoutIdMaybe, +): ChangeUserBoundingBoxAction => ({ + type: "CHANGE_USER_BOUNDING_BOX", id, - bounds, + newProps, }); export const finishedResizingUserBoundingBoxAction = ( @@ -296,7 +274,7 @@ export const finishedResizingUserBoundingBoxAction = ( }); export const addUserBoundingBoxAction = ( - newBoundingBox?: ?UserBoundingBox, + newBoundingBox?: ?UserBoundingBoxWithoutId, center?: Vector3, ): AddNewUserBoundingBox => ({ type: "ADD_NEW_USER_BOUNDING_BOX", @@ -304,33 +282,6 @@ export const addUserBoundingBoxAction = ( center, }); -export const setUserBoundingBoxVisibilityAction = ( - id: number, - isVisible: boolean, -): SetUserBoundingBoxVisibilityAction => ({ - type: "SET_USER_BOUNDING_BOX_VISIBILITY", - id, - isVisible, -}); - -export const setUserBoundingBoxNameAction = ( - id: number, - name: string, -): SetUserBoundingBoxNameAction => ({ - type: "SET_USER_BOUNDING_BOX_NAME", - id, - name, -}); - -export const setUserBoundingBoxColorAction = ( - id: number, - color: Vector3, -): SetUserBoundingBoxColorAction => ({ - type: "SET_USER_BOUNDING_BOX_COLOR", - id, - color, -}); - export const deleteUserBoundingBoxAction = (id: number): DeleteUserBoundingBox => ({ type: "DELETE_USER_BOUNDING_BOX", id, diff --git a/frontend/javascripts/oxalis/model/actions/ui_actions.js b/frontend/javascripts/oxalis/model/actions/ui_actions.js index df97a412938..59f7e21c868 100644 --- a/frontend/javascripts/oxalis/model/actions/ui_actions.js +++ b/frontend/javascripts/oxalis/model/actions/ui_actions.js @@ -3,9 +3,9 @@ import type { AnnotationTool } from "oxalis/constants"; import { type BorderOpenStatus, type Theme } from "oxalis/store"; -type SetToolAction = { type: "SET_TOOL", tool: AnnotationTool }; +export type SetToolAction = { type: "SET_TOOL", tool: AnnotationTool }; -type CycleToolAction = { type: "CYCLE_TOOL" }; +export type CycleToolAction = { type: "CYCLE_TOOL" }; type SetDropzoneModalVisibilityAction = { type: "SET_DROPZONE_MODAL_VISIBILITY", diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index e51802bcc7a..9a5e3b6ce90 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -4,6 +4,7 @@ import update from "immutability-helper"; import type { Action } from "oxalis/model/actions/actions"; import type { OxalisState, UserBoundingBox } from "oxalis/store"; +import { V3 } from "libs/mjs"; import { type StateShape1, updateKey, @@ -13,7 +14,6 @@ import { import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import * as Utils from "libs/utils"; import { getDisplayedDataExtentInPlaneMode } from "oxalis/model/accessors/view_mode_accessor"; -import { map3 } from "libs/utils"; import { convertServerAnnotationToFrontendAnnotation } from "oxalis/model/reducers/reducer_helpers"; const updateTracing = (state: OxalisState, shape: StateShape1<"tracing">): OxalisState => @@ -75,7 +75,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, action.userBoundingBoxes); } - case "SET_USER_BOUNDING_BOX_BOUNDS": { + case "CHANGE_USER_BOUNDING_BOX": { const tracing = maybeGetSomeTracing(state.tracing); if (tracing == null) { return state; @@ -83,8 +83,9 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => bbox.id === action.id ? { + id: bbox.id, ...bbox, - boundingBox: action.bounds, + ...action.newProps, } : bbox, ); @@ -99,32 +100,26 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { const { userBoundingBoxes } = tracing; const highestBoundingBoxId = Math.max(0, ...userBoundingBoxes.map(bb => bb.id)); const boundingBoxId = highestBoundingBoxId + 1; - let { newBoundingBox } = action; - if (newBoundingBox != null) { - newBoundingBox.id = boundingBoxId; + let newBoundingBox: UserBoundingBox; + if (action.newBoundingBox != null) { + newBoundingBox = ({ id: boundingBoxId, ...action.newBoundingBox }: UserBoundingBox); } else { const { min, max, halfBoxExtent } = getDisplayedDataExtentInPlaneMode(state); newBoundingBox = { boundingBox: { min, max }, id: boundingBoxId, - name: `bounding box ${boundingBoxId}`, + name: `Bounding box ${boundingBoxId}`, color: Utils.getRandomColor(), isVisible: true, }; if (action.center != null) { - const roundedCenter = map3(val => Math.round(val), action.center); - const roundedHalfBoxExtent = map3(val => Math.round(val), halfBoxExtent); newBoundingBox.boundingBox = { - min: [ - roundedCenter[0] - roundedHalfBoxExtent[0], - roundedCenter[1] - roundedHalfBoxExtent[1], - roundedCenter[2] - roundedHalfBoxExtent[2], - ], - max: [ - roundedCenter[0] + roundedHalfBoxExtent[0], - roundedCenter[1] + roundedHalfBoxExtent[1], - roundedCenter[2] + roundedHalfBoxExtent[2], - ], + min: V3.sub(action.center, halfBoxExtent) + .round() + .toArray(), + max: V3.add(action.center, halfBoxExtent) + .round() + .toArray(), }; } } @@ -137,11 +132,11 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { if (tracing == null) { return state; } - let highestBoundingBoxId = Math.max(0, ...tracing.userBoundingBoxes.map(bb => bb.id)); - const additionalUserBoundingBoxes = action.userBoundingBoxes.map(bb => { - highestBoundingBoxId++; - return { ...bb, id: highestBoundingBoxId }; - }); + const highestBoundingBoxId = Math.max(0, ...tracing.userBoundingBoxes.map(bb => bb.id)); + const additionalUserBoundingBoxes = action.userBoundingBoxes.map((bb, index) => ({ + ...bb, + id: highestBoundingBoxId + index + 1, + })); const mergedUserBoundingBoxes = [ ...tracing.userBoundingBoxes, ...additionalUserBoundingBoxes, @@ -160,39 +155,6 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); } - case "SET_USER_BOUNDING_BOX_VISIBILITY": { - const tracing = maybeGetSomeTracing(state.tracing); - if (tracing == null) { - return state; - } - const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => - bbox.id !== action.id ? bbox : { ...bbox, isVisible: action.isVisible }, - ); - return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); - } - - case "SET_USER_BOUNDING_BOX_NAME": { - const tracing = maybeGetSomeTracing(state.tracing); - if (tracing == null) { - return state; - } - const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => - bbox.id !== action.id ? bbox : { ...bbox, name: action.name }, - ); - return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); - } - - case "SET_USER_BOUNDING_BOX_COLOR": { - const tracing = maybeGetSomeTracing(state.tracing); - if (tracing == null) { - return state; - } - const updatedUserBoundingBoxes = tracing.userBoundingBoxes.map(bbox => - bbox.id !== action.id ? bbox : { ...bbox, color: action.color }, - ); - return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); - } - case "UPDATE_REMOTE_MESH_METADATA": { const { id, meshShape } = action; const newMeshes = state.tracing.meshes.map(mesh => { diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js index 09b381e8767..d7fabb7d8ce 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -15,10 +15,12 @@ import type { OxalisState, } from "oxalis/store"; import type { Boundary } from "oxalis/model/accessors/dataset_accessor"; +import { AnnotationToolEnum } from "oxalis/constants"; import type { BoundingBoxType, AnnotationTool } from "oxalis/constants"; import { V3 } from "libs/mjs"; import * as Utils from "libs/utils"; import { getToolClassForAnnotationTool } from "oxalis/controller/combinations/tool_controls"; +import { getDisabledInfoForTools } from "oxalis/model/accessors/tool_accessor"; import { isVolumeTool, isVolumeAnnotationDisallowedForZoom, @@ -54,7 +56,7 @@ export function convertUserBoundingBoxesFromServerToFrontend( boundingBox: convertedBoundingBox, color: color ? Utils.colorObjectToRGBArray(color) : Utils.getRandomColor(), id, - name: name || `bounding box ${id}`, + name: name || `Bounding box ${id}`, isVisible: isVisible != null ? isVisible : true, }; }); @@ -136,6 +138,26 @@ export function convertServerAnnotationToFrontendAnnotation(annotation: APIAnnot }; } +export function getNextTool(state: OxalisState): AnnotationTool | null { + const disabledToolInfo = getDisabledInfoForTools(state); + + const tools = Object.keys(AnnotationToolEnum); + const currentToolIndex = tools.indexOf(state.uiInformation.activeTool); + + // Search for the next tool which is not disabled. + for ( + let newToolIndex = currentToolIndex + 1; + newToolIndex < currentToolIndex + tools.length; + newToolIndex++ + ) { + const newTool = tools[newToolIndex % tools.length]; + if (!disabledToolInfo[newTool].isDisabled) { + return newTool; + } + } + return null; +} + export function setToolReducer(state: OxalisState, tool: AnnotationTool) { if (tool === state.uiInformation.activeTool) { return state; diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.js b/frontend/javascripts/oxalis/model/reducers/ui_reducer.js index 0cc061e674f..d18e67703bc 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.js @@ -3,10 +3,8 @@ import type { Action } from "oxalis/model/actions/actions"; import type { OxalisState } from "oxalis/store"; import { updateKey } from "oxalis/model/helpers/deep_update"; -import { setToolReducer } from "oxalis/model/reducers/reducer_helpers"; -import { AnnotationToolEnum } from "oxalis/constants"; +import { setToolReducer, getNextTool } from "oxalis/model/reducers/reducer_helpers"; import { hideBrushReducer } from "oxalis/model/reducers/volumetracing_reducer_helpers"; -import { getDisabledInfoForTools } from "oxalis/model/accessors/tool_accessor"; function UiReducer(state: OxalisState, action: Action): OxalisState { switch (action.type) { @@ -50,25 +48,12 @@ function UiReducer(state: OxalisState, action: Action): OxalisState { if (!state.tracing.restrictions.allowUpdate) { return state; } - - const disabledToolInfo = getDisabledInfoForTools(state); - - const tools = Object.keys(AnnotationToolEnum); - const currentToolIndex = tools.indexOf(state.uiInformation.activeTool); - - // Search for the next tool which is not disabled. - for ( - let newToolIndex = currentToolIndex + 1; - newToolIndex < currentToolIndex + tools.length; - newToolIndex++ - ) { - const newTool = tools[newToolIndex % tools.length]; - if (!disabledToolInfo[newTool].isDisabled) { - return setToolReducer(hideBrushReducer(state), newTool); - } + const nextTool = getNextTool(state); + if (nextTool == null) { + // Don't change the current tool if another tool could not be selected. + return state; } - // Don't change the current tool if another tool could not be selected. - return state; + return setToolReducer(hideBrushReducer(state), nextTool); } case "SET_THEME": { diff --git a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js new file mode 100644 index 00000000000..22d364a4711 --- /dev/null +++ b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js @@ -0,0 +1,23 @@ +// @flow +/* eslint-disable import/prefer-default-export */ +import { type Saga, _takeEvery, select } from "oxalis/model/sagas/effect-generators"; +import type { SetToolAction, CycleToolAction } from "oxalis/model/actions/ui_actions"; +import { getNextTool } from "oxalis/model/reducers/reducer_helpers"; +import { getToolClassForAnnotationTool } from "oxalis/controller/combinations/tool_controls"; + +export function* watchToolDeselection(): Saga { + function* triggerToolDeselection(action: SetToolAction | CycleToolAction): Saga { + const storeState = yield* select(state => state); + let executeDeselect = false; + if (action.type === "SET_TOOL") { + executeDeselect = true; + } else if (getNextTool(storeState) != null) { + executeDeselect = true; + } + if (executeDeselect) { + getToolClassForAnnotationTool(storeState.uiInformation.activeTool).onToolDeselected(); + } + } + + yield _takeEvery(["CYCLE_TOOL", "SET_TOOL"], triggerToolDeselection); +} diff --git a/frontend/javascripts/oxalis/model/sagas/root_saga.js b/frontend/javascripts/oxalis/model/sagas/root_saga.js index 862a05cd62d..e03fbe2f4cb 100644 --- a/frontend/javascripts/oxalis/model/sagas/root_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/root_saga.js @@ -27,6 +27,7 @@ import ErrorHandling from "libs/error_handling"; import handleMeshChanges from "oxalis/model/sagas/handle_mesh_changes"; import isosurfaceSaga from "oxalis/model/sagas/isosurface_saga"; import { watchMaximumRenderableLayers } from "oxalis/model/sagas/dataset_saga"; +import { watchToolDeselection } from "oxalis/model/sagas/annotation_tool_saga"; import watchPushSettingsAsync from "oxalis/model/sagas/settings_saga"; import watchTasksAsync, { warnAboutMagRestriction } from "oxalis/model/sagas/task_saga"; import loadHistogramData from "oxalis/model/sagas/load_histogram_data_saga"; @@ -69,6 +70,7 @@ function* restartableSaga(): Saga { _call(watchMaximumRenderableLayers), _call(watchActivatedMappings), _call(watchAgglomerateLoading), + _call(watchToolDeselection), ]); } catch (err) { rootSagaCrashed = true; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index a6ec1b1f674..d53de4d7f8b 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -108,7 +108,7 @@ type UndoBucket = { type VolumeAnnotationBatch = Array; type SkeletonUndoState = { type: "skeleton", data: SkeletonTracing }; type VolumeUndoState = { type: "volume", data: VolumeAnnotationBatch }; -type BoundingBoxUndoState = { type: "bounding box", data: Array }; +type BoundingBoxUndoState = { type: "bounding_box", data: Array }; type WarnUndoState = { type: "warning", reason: string }; type UndoState = SkeletonUndoState | VolumeUndoState | BoundingBoxUndoState | WarnUndoState; @@ -322,10 +322,11 @@ function getBoundingBoxToUndoState( previousAction.id != null && userBoundingBoxAction.type === previousAction.type && userBoundingBoxAction.id === previousAction.id; + // Used to distinguish between different resizing actions of the same bounding box. const isFinishedResizingAction = userBoundingBoxAction.type === "FINISHED_RESIZING_USER_BOUNDING_BOX"; if (!isSameActionOnSameBoundingBox && !isFinishedResizingAction) { - return { type: "bounding box", data: prevUserBoundingBoxes }; + return { type: "bounding_box", data: prevUserBoundingBoxes }; } return null; } @@ -443,9 +444,9 @@ function* applyStateOfStack( const currentVolumeState = yield* call(applyAndGetRevertingVolumeBatch, volumeBatchToApply); stackToPushTo.push(currentVolumeState); yield* call(progressCallback, true, `Finished ${direction}...`); - } else if (stateToRestore.type === "bounding box") { + } else if (stateToRestore.type === "bounding_box") { if (prevUserBoundingBoxes != null) { - stackToPushTo.push({ type: "bounding box", data: prevUserBoundingBoxes }); + stackToPushTo.push({ type: "bounding_box", data: prevUserBoundingBoxes }); } const newBoundingBoxes = stateToRestore.data; yield* put(setUserBoundingBoxesAction(newBoundingBoxes)); diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index bdcdeaff9d4..8cffce4ac1f 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -595,8 +595,6 @@ export function* floodFill(): Saga { yield* put( addUserBoundingBoxAction({ - // The id will be set by the action. - id: 0, boundingBox: coveredBoundingBox, name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( ",", diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index 6e0cc41691d..2212790f154 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -113,14 +113,24 @@ export type UserBoundingBoxToServer = { isVisible?: boolean, }; -export type UserBoundingBox = { - id: number, +export type UserBoundingBoxWithoutIdMaybe = {| + boundingBox?: BoundingBoxType, + name?: string, + color?: Vector3, + isVisible?: boolean, +|}; + +export type UserBoundingBoxWithoutId = {| boundingBox: BoundingBoxType, name: string, color: Vector3, isVisible: boolean, -}; +|}; +export type UserBoundingBox = { + id: number, + ...UserBoundingBoxWithoutId, +}; export type MutableTree = {| treeId: number, groupId: ?number, diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js index f4c4f93ccae..842c52a8acf 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js @@ -10,6 +10,7 @@ import Constants, { type OverwriteMode, OverwriteModeEnum, FillModeEnum, + VolumeTools, } from "oxalis/constants"; import { convertCellIdToCSS } from "oxalis/view/left-border-tabs/mapping_settings_view"; import { document } from "libs/window"; @@ -257,7 +258,7 @@ function CreateCellButton() { function CreateNewBoundingBoxButton() { return ( - + Trace Tool Icon | null, + userBoundingBoxes: Array, |}; /* eslint-enable react/no-unused-prop-types */ @@ -303,27 +301,139 @@ function NodeContextMenuOptions({ ); } -function NoNodeContextMenuOptions({ - skeletonTracing, - volumeTracing, - activeTool, - hideContextMenu, +function getBoundingBoxMenuOptions({ + addNewBoundingBox, globalPosition, - viewport, - createTree, - segmentIdAtPosition, - visibleSegmentationLayer, + activeTool, clickedBoundingBoxId, - dataset, - currentMeshFile, - setActiveCell, userBoundingBoxes, - addNewBoundingBox, - hideBoundingBox, setBoundingBoxName, + hideContextMenu, setBoundingBoxColor, + hideBoundingBox, deleteBoundingBox, }: NoNodeContextMenuProps) { + const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; + const newBoundingBoxMenuItem = ( + { + addNewBoundingBox(globalPosition); + }} + > + Create new Bounding Box + {isBoundingBoxToolActive ? shortcutBuilder(["C"]) : null} + + ); + if (clickedBoundingBoxId == null) { + return [newBoundingBoxMenuItem]; + } + const hoveredBBox = userBoundingBoxes.find(bbox => bbox.id === clickedBoundingBoxId); + if (hoveredBBox == null) { + return [newBoundingBoxMenuItem]; + } + const setBBoxName = (evt: SyntheticInputEvent<>) => { + setBoundingBoxName(clickedBoundingBoxId, evt.target.value); + }; + const preventContextMenuFromClosing = evt => { + evt.stopPropagation(); + }; + const upscaledBBoxColor = ((hoveredBBox.color.map(colorPart => colorPart * 255): any): Vector3); + return [ + newBoundingBoxMenuItem, + + { + setBBoxName(evt); + hideContextMenu(); + }} + onBlur={setBBoxName} + onClick={preventContextMenuFromClosing} + /> + } + trigger="click" + > + + Change Bounding Box Name + + + , + + + Change Bounding Box Color + ) => { + let color = hexToRgb(evt.target.value); + color = ((color.map(colorPart => colorPart / 255): any): Vector3); + setBoundingBoxColor(clickedBoundingBoxId, color); + }} + onBlur={() => hideContextMenu} + value={rgbToHex(upscaledBBoxColor)} + /> + + , + { + hideBoundingBox(clickedBoundingBoxId); + }} + > + Hide Bounding Box + , + { + deleteBoundingBox(clickedBoundingBoxId); + }} + > + Delete Bounding Box + , + ]; +} + +function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { + const { + skeletonTracing, + volumeTracing, + activeTool, + globalPosition, + viewport, + createTree, + segmentIdAtPosition, + visibleSegmentationLayer, + dataset, + currentMeshFile, + setActiveCell, + } = props; + useEffect(() => { (async () => { await maybeFetchMeshFiles(visibleSegmentationLayer, dataset, false); @@ -413,109 +523,8 @@ function NoNodeContextMenuOptions({ : []; const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; - let boundingBoxActions = [ - { - addNewBoundingBox(globalPosition); - }} - > - Create new Bounding Box - {isBoundingBoxToolActive ? shortcutBuilder(["C"]) : null} - , - ]; - if (isBoundingBoxToolActive && clickedBoundingBoxId != null && userBoundingBoxes != null) { - const hoveredBBox = userBoundingBoxes.find(bbox => bbox.id === clickedBoundingBoxId); - if (hoveredBBox) { - const setBBoxName = (evt: SyntheticInputEvent<>) => { - setBoundingBoxName(clickedBoundingBoxId, evt.target.value); - }; - const preventContextMenuFromClosing = evt => { - evt.stopPropagation(); - }; - const upscaledBBoxColor = ((hoveredBBox.color.map( - colorPart => colorPart * 255, - ): any): Vector3); - boundingBoxActions = [ - ...boundingBoxActions, - - { - setBBoxName(evt); - hideContextMenu(); - }} - onBlur={setBBoxName} - onClick={preventContextMenuFromClosing} - /> - } - trigger="click" - > - - Change Bounding Box Name - - - , - - - Change Bounding Box Color - ) => { - let color = hexToRgb(evt.target.value); - color = ((color.map(colorPart => colorPart / 255): any): Vector3); - setBoundingBoxColor(clickedBoundingBoxId, color); - }} - onBlur={() => hideContextMenu} - value={rgbToHex(upscaledBBoxColor)} - /> - - , - { - hideBoundingBox(clickedBoundingBoxId); - }} - > - Hide Bounding Box - , - { - deleteBoundingBox(clickedBoundingBoxId); - }} - > - Delete Bounding Box - , - ]; - } - } + const boundingBoxActions = getBoundingBoxMenuOptions(props); + if (volumeTracing == null && visibleSegmentationLayer != null) { nonSkeletonActions.push(loadMeshItem); } @@ -534,14 +543,7 @@ function NoNodeContextMenuOptions({ } return ( - { - console.log("hiding context menu", args); - hideContextMenu(); - }} - style={{ borderRadius: 6 }} - mode="vertical" - > + {allActions} ); @@ -705,16 +707,16 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ dispatch(addUserBoundingBoxAction(null, center)); }, setBoundingBoxName(id: number, name: string) { - dispatch(setUserBoundingBoxNameAction(id, name)); + dispatch(changeUserBoundingBoxAction(id, { name })); }, setBoundingBoxColor(id: number, color: Vector3) { - dispatch(setUserBoundingBoxColorAction(id, color)); + dispatch(changeUserBoundingBoxAction(id, { color })); }, deleteBoundingBox(id: number) { dispatch(deleteUserBoundingBoxAction(id)); }, hideBoundingBox(id: number) { - dispatch(setUserBoundingBoxVisibilityAction(id, false)); + dispatch(changeUserBoundingBoxAction(id, { isVisible: false })); }, }); @@ -734,7 +736,7 @@ function mapStateToProps(state: OxalisState): StateProps { ? state.currentMeshFileByLayer[visibleSegmentationLayer.name] : null, useLegacyBindings: state.userConfiguration.useLegacyBindings, - userBoundingBoxes: someTracing != null ? someTracing.userBoundingBoxes : null, + userBoundingBoxes: someTracing != null ? someTracing.userBoundingBoxes : [], }; } diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js index 05fafcf18b2..d5ab449ba6b 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js @@ -248,15 +248,7 @@ class TracingLayoutView extends React.PureComponent { ); } - const { - clickedNodeId, - clickedBoundingBoxId, - contextMenuPosition, - contextMenuGlobalPosition, - contextMenuViewport, - status, - activeLayoutName, - } = this.state; + const { contextMenuPosition, contextMenuViewport, status, activeLayoutName } = this.state; const layoutType = determineLayout( this.props.initialCommandType.type, @@ -282,10 +274,10 @@ class TracingLayoutView extends React.PureComponent { {contextMenuPosition != null && contextMenuViewport != null ? ( ) : null} diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js index 5ea0fbbf977..8cf8ed94df0 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.js @@ -4,70 +4,46 @@ */ import { Tooltip } from "antd"; import { PlusSquareOutlined } from "@ant-design/icons"; -import type { Dispatch } from "redux"; -import { connect } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import React, { useState } from "react"; import _ from "lodash"; -import type { APIDataset } from "types/api_flow_types"; import { setPositionAction } from "oxalis/model/actions/flycam_actions"; import type { Vector3, Vector6, BoundingBoxType } from "oxalis/constants"; import { UserBoundingBoxInput } from "oxalis/view/components/setting_input_views"; -import type { OxalisState, Tracing } from "oxalis/store"; import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import { - setUserBoundingBoxBoundsAction, + changeUserBoundingBoxAction, addUserBoundingBoxAction, deleteUserBoundingBoxAction, - setUserBoundingBoxVisibilityAction, - setUserBoundingBoxNameAction, - setUserBoundingBoxColorAction, } from "oxalis/model/actions/annotation_actions"; import * as Utils from "libs/utils"; import ExportBoundingBoxModal from "oxalis/view/right-border-tabs/export_bounding_box_modal"; -type BoundingBoxTabProps = { - tracing: Tracing, - setChangeBoundingBoxBounds: (number, BoundingBoxType) => void, - addNewBoundingBox: () => void, - deleteBoundingBox: number => void, - setBoundingBoxVisibility: (number, boolean) => void, - setBoundingBoxName: (number, string) => void, - setBoundingBoxColor: (number, Vector3) => void, - setPosition: Vector3 => void, - dataset: APIDataset, -}; - -function BoundingBoxTab(props: BoundingBoxTabProps) { +export default function BoundingBoxTab() { const [selectedBoundingBoxForExport, setSelectedBoundingBoxForExport] = useState(null); - const { - tracing, - dataset, - setChangeBoundingBoxBounds, - addNewBoundingBox, - setBoundingBoxVisibility, - setBoundingBoxName, - setBoundingBoxColor, - deleteBoundingBox, - setPosition, - } = props; + const tracing = useSelector(state => state.tracing); + const dataset = useSelector(state => state.dataset); const { userBoundingBoxes } = getSomeTracing(tracing); - function handleBoundingBoxBoundingChange(id: number, boundingBox: Vector6) { - setChangeBoundingBoxBounds(id, Utils.computeBoundingBoxFromArray(boundingBox)); - } + const dispatch = useDispatch(); + const setChangeBoundingBoxBounds = (id: number, boundingBox: BoundingBoxType) => + dispatch(changeUserBoundingBoxAction(id, { boundingBox })); + const addNewBoundingBox = () => dispatch(addUserBoundingBoxAction()); - function handleBoundingBoxVisibilityChange(id: number, isVisible: boolean) { - setBoundingBoxVisibility(id, isVisible); - } + const setPosition = (position: Vector3) => dispatch(setPositionAction(position)); - function handleBoundingBoxNameChange(id: number, name: string) { - setBoundingBoxName(id, name); - } + const deleteBoundingBox = (id: number) => dispatch(deleteUserBoundingBoxAction(id)); + const setBoundingBoxVisibility = (id: number, isVisible: boolean) => + dispatch(changeUserBoundingBoxAction(id, { isVisible })); + const setBoundingBoxName = (id: number, name: string) => + dispatch(changeUserBoundingBoxAction(id, { name })); + const setBoundingBoxColor = (id: number, color: Vector3) => + dispatch(changeUserBoundingBoxAction(id, { color })); - function handleBoundingBoxColorChange(id: number, color: Vector3) { - setBoundingBoxColor(id, color); + function handleBoundingBoxBoundingChange(id: number, boundingBox: Vector6) { + setChangeBoundingBoxBounds(id, Utils.computeBoundingBoxFromArray(boundingBox)); } function handleGoToBoundingBox(id: number) { @@ -84,14 +60,6 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { setPosition(center); } - function handleAddNewUserBoundingBox() { - addNewBoundingBox(); - } - - function handleDeleteUserBoundingBox(id: number) { - deleteBoundingBox(id); - } - return (
{userBoundingBoxes.length > 0 ? ( @@ -105,14 +73,14 @@ function BoundingBoxTab(props: BoundingBoxTabProps) { isExportEnabled={dataset.jobsEnabled} isVisible={bb.isVisible} onBoundingChange={_.partial(handleBoundingBoxBoundingChange, bb.id)} - onDelete={_.partial(handleDeleteUserBoundingBox, bb.id)} + onDelete={_.partial(deleteBoundingBox, bb.id)} onExport={ dataset.jobsEnabled ? _.partial(setSelectedBoundingBoxForExport, bb) : () => {} } onGoToBoundingBox={_.partial(handleGoToBoundingBox, bb.id)} - onVisibilityChange={_.partial(handleBoundingBoxVisibilityChange, bb.id)} - onNameChange={_.partial(handleBoundingBoxNameChange, bb.id)} - onColorChange={_.partial(handleBoundingBoxColorChange, bb.id)} + onVisibilityChange={_.partial(setBoundingBoxVisibility, bb.id)} + onNameChange={_.partial(setBoundingBoxName, bb.id)} + onColorChange={_.partial(setBoundingBoxColor, bb.id)} /> )) ) : ( @@ -121,7 +89,7 @@ function BoundingBoxTab(props: BoundingBoxTabProps) {
); } - -const mapStateToProps = (state: OxalisState) => ({ - tracing: state.tracing, - dataset: state.dataset, -}); - -const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ - setChangeBoundingBoxBounds(id: number, bounds: BoundingBoxType) { - dispatch(setUserBoundingBoxBoundsAction(id, bounds)); - }, - addNewBoundingBox() { - dispatch(addUserBoundingBoxAction()); - }, - setPosition(position: Vector3) { - dispatch(setPositionAction(position)); - }, - deleteBoundingBox(id: number) { - dispatch(deleteUserBoundingBoxAction(id)); - }, - setBoundingBoxVisibility(id: number, isVisible: boolean) { - dispatch(setUserBoundingBoxVisibilityAction(id, isVisible)); - }, - setBoundingBoxName(id: number, name: string) { - dispatch(setUserBoundingBoxNameAction(id, name)); - }, - setBoundingBoxColor(id: number, color: Vector3) { - dispatch(setUserBoundingBoxColorAction(id, color)); - }, -}); - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(BoundingBoxTab); diff --git a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js index 222635c3079..e91cc010b37 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js @@ -262,7 +262,8 @@ test.serial("Executing a floodfill in mag 1 (long operation)", async t => { // Assert state after flood-fill await assertFloodFilledState(); - // Undo and assert initial state + // Undo created bounding box by flood fill and flood fill and assert initial state + await dispatchUndoAsync(Store.dispatch); await dispatchUndoAsync(Store.dispatch); await assertInitialState(); From 9e31ef2bd3e1fb8de56472b74af5c1edbfd00815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Mon, 1 Nov 2021 18:13:46 +0100 Subject: [PATCH 26/34] fix cyclic imports --- frontend/javascripts/oxalis/model/reducers/reducer_helpers.js | 3 --- .../test/sagas/volumetracing_saga_integration.spec.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js index d7fabb7d8ce..d1601d17225 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -19,7 +19,6 @@ import { AnnotationToolEnum } from "oxalis/constants"; import type { BoundingBoxType, AnnotationTool } from "oxalis/constants"; import { V3 } from "libs/mjs"; import * as Utils from "libs/utils"; -import { getToolClassForAnnotationTool } from "oxalis/controller/combinations/tool_controls"; import { getDisabledInfoForTools } from "oxalis/model/accessors/tool_accessor"; import { isVolumeTool, @@ -165,7 +164,5 @@ export function setToolReducer(state: OxalisState, tool: AnnotationTool) { if (isVolumeTool(tool) && isVolumeAnnotationDisallowedForZoom(tool, state)) { return state; } - // Execute deselection event. - getToolClassForAnnotationTool(state.uiInformation.activeTool).onToolDeselected(); return updateKey(state, "uiInformation", { activeTool: tool }); } diff --git a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js index e91cc010b37..b8704182d65 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js @@ -515,7 +515,7 @@ test.serial( ); test.serial( - "Brushing/Tracing should send buckets to backend and restore dirty flag afterwards", + "Brushing/Tracing should not crash when too many buckets are labeled at once with saving inbetween", async t => { await t.context.api.tracing.save(); From 82c2ce9aab125c74733a0fa367090795c245fb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 4 Nov 2021 18:13:25 +0100 Subject: [PATCH 27/34] fix little bug caused by refactoring - and make ui more expressive via tooltips --- .../oxalis/model/reducers/annotation_reducer.js | 8 ++------ .../javascripts/oxalis/view/action-bar/toolbar_view.js | 2 +- .../oxalis/view/components/setting_input_views.js | 4 +++- frontend/javascripts/oxalis/view/context_menu.js | 9 +++++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index ab4817dc113..40e17a03c61 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -114,12 +114,8 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { }; if (action.center != null) { newBoundingBox.boundingBox = { - min: V3.sub(action.center, halfBoxExtent) - .round() - .toArray(), - max: V3.add(action.center, halfBoxExtent) - .round() - .toArray(), + min: V3.toArray(V3.round(V3.sub(action.center, halfBoxExtent))), + max: V3.toArray(V3.round(V3.add(action.center, halfBoxExtent))), }; } } diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js index 842c52a8acf..e84547e24ae 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js @@ -578,7 +578,7 @@ export default function ToolbarView() { /> - + + + - Create new Tree here {!isVolumeBasedToolActive ? shortcutBuilder(["C"]) : null} + Create new Tree here{" "} + {!isVolumeBasedToolActive && !isBoundingBoxToolActive ? shortcutBuilder(["C"]) : null} , ] : []; @@ -549,7 +552,6 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { ] : []; - const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; const boundingBoxActions = getBoundingBoxMenuOptions(props); if (volumeTracing == null && visibleSegmentationLayer != null) { @@ -571,7 +573,7 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { } return ( - + {allActions} ); @@ -709,7 +711,6 @@ function ContextMenu(props: Props) { ); } -// TODO: Refactor this !!!!!!!!!!, above useDispatch is already used. const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ deleteEdge(firstNodeId: number, secondNodeId: number) { dispatch(deleteEdgeAction(firstNodeId, secondNodeId)); From b4bcfb39245b4aa3d8db5d38149cb96fb4df7a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Thu, 4 Nov 2021 18:17:33 +0100 Subject: [PATCH 28/34] add changelog entry --- CHANGELOG.unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 728b6eead0a..4557718e746 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -14,6 +14,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - 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) - Added the possibility to load the skeletons of specific agglomerates from an agglomerate file when opening a tracing by including a mapping and agglomerate ids in the URL hash. See the [docs](https://docs.webknossos.org/webknossos/sharing.html#sharing-link-format) for further information. [#5738](https://github.com/scalableminds/webknossos/pull/5738) - Added a skeleton sandbox mode where a dataset can be opened and all skeleton tracing capabilities are available. However, by default changes are not saved. At any point, users can decide to copy the current state to their account. The sandbox can be accessed at `/datasets///sandbox/skeleton`. In the combination with the new agglomerate skeleton loading feature this can be used to craft links that open webknossos with an activated mapping and specific agglomerates loaded on-demand. [#5738](https://github.com/scalableminds/webknossos/pull/5738) +- Added a new bounding box tool that allows resizing and creating bounding boxes more easily. Additionally, the context menu now contains options to modify the bounding box close to the clicked position. [#5767](https://github.com/scalableminds/webknossos/pull/5767) - The active mapping is now included in the link copied from the "Share" modal or the new "Share" button next to the dataset position. It is automatically activated for users that open the shared link. [#5738](https://github.com/scalableminds/webknossos/pull/5738) - A new "Segments" tab was added which replaces the old "Meshes" tab. The tab renders a list of segments within a volume annotation for the visible segmentation layer. The list "grows" while creating an annotation or browsing a dataset. For example, selecting an existing segment or drawing with a new segment id will both ensure that the segment is listed. Via right-click, meshes can be loaded for a selected segment. The mesh will be added as child to the segment. [#5696](https://github.com/scalableminds/webknossos/pull/5696) - For ad-hoc mesh computation and for mesh precomputation, the user can now select which quality the mesh should have (i.e., via selecting which magnification should be used). [#5696](https://github.com/scalableminds/webknossos/pull/5696) From bd08c5fe5827818820cc24e4d4a30d3f738c9835 Mon Sep 17 00:00:00 2001 From: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Sat, 6 Nov 2021 11:39:43 +0100 Subject: [PATCH 29/34] Apply suggestions from code review Co-authored-by: Philipp Otto --- .../oxalis/controller/combinations/bounding_box_handlers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index 50c9004e09a..15028d22f87 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -40,8 +40,8 @@ function getDistanceToBoundingBoxEdge( // This method calculates the distance between the given pos and a single edge of a cross section // of bounding box given min and max that is displayed in a certain viewport. // The edge dimension that the edge extents along is given by the first entry of primaryAndSecondaryDim. - // Here goes 0 = x direction, 1 = y direction and 2 = z direction. - // The second entry of primaryAndSecondaryDim gives the other extend of the viewport / cross section. + // Namely, 0 -> x direction (i.e., horizontal), 1 -> y direction and 2 -> z direction. + // The second entry of primaryAndSecondaryDim gives the other extent of the viewport / cross section. // The boolean compareToMin tells which of the two edges in the primary direction should be compared with. // To calculate the distance there are three cases depending on the primary dimension value of pos. // - One when the given pos is on the left of the minimum of the edge (case 1), @@ -130,7 +130,7 @@ function computeDistanceArray( // Return the edge or edges of the bounding box closest to the mouse position if their distance is below a certain threshold. // If no edge is close to the mouse null is returned instead. Otherwise the first entry is always the closest edge. -// If the mouse near a corner, there is always an additional edge that is close to the mouse. +// If the mouse is near a corner, there is always an additional edge that is close to the mouse. // If such an edge exists then this edge is the second entry of the array. // If the mouse isn't close to a corner of a crossection, the second entry is null. export function getClosestHoveredBoundingBox( From 6c95d89a5d7838e8f4a7d57067a43968b8ac4681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Sat, 6 Nov 2021 12:30:00 +0100 Subject: [PATCH 30/34] make distance to bounding box dimension independent --- .../combinations/bounding_box_handlers.js | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index 50c9004e09a..ec1d917869f 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -65,32 +65,33 @@ function getDistanceToBoundingBoxEdge( // the distance between the mouse and the bounding box is distorted by the factor of planeRatio. // That's why we later divide exactly by this factor to let the hit box / distance // between the mouse and bounding box be the same in each dimension. - const [primaryEdgeDim, secondaryprimaryEdgeDim] = primaryAndSecondaryDim; + const [primaryEdgeDim, secondaryEdgeDim] = primaryAndSecondaryDim; const cornerToCompareWith = compareToMin ? min : max; + const toScreenSpace = (value: number, dimension: DimensionIndices) => + value / planeRatio[dimension]; if (pos[primaryEdgeDim] < min[primaryEdgeDim]) { // Case 1: Distance to the min corner is needed in primaryEdgeDim. - return ( - Math.hypot( - pos[primaryEdgeDim] - min[primaryEdgeDim], - pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim], - ) / planeRatio[primaryEdgeDim] + return Math.hypot( + toScreenSpace(pos[primaryEdgeDim] - min[primaryEdgeDim], primaryEdgeDim), + toScreenSpace( + pos[secondaryEdgeDim] - cornerToCompareWith[secondaryEdgeDim], + secondaryEdgeDim, + ), ); } if (pos[primaryEdgeDim] > max[primaryEdgeDim]) { // Case 2: Distance to max Corner is needed in primaryEdgeDim. - return ( - Math.hypot( - pos[primaryEdgeDim] - max[primaryEdgeDim], - pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim], - ) / planeRatio[primaryEdgeDim] + return Math.hypot( + toScreenSpace(pos[primaryEdgeDim] - max[primaryEdgeDim], primaryEdgeDim), + toScreenSpace( + pos[secondaryEdgeDim] - cornerToCompareWith[secondaryEdgeDim], + secondaryEdgeDim, + ), ); } // Case 3: - // If the position is within the bounds of the primaryEdgeDim, the shortest distance - // to the edge is simply the difference between the secondaryprimaryEdgeDim values. - return ( - Math.abs(pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim]) / - planeRatio[primaryEdgeDim] + return Math.abs( + toScreenSpace(pos[secondaryEdgeDim] - cornerToCompareWith[secondaryEdgeDim], secondaryEdgeDim), ); } From 86dfab0ae22e0cfbda0eb34b0e663b07d3db3a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Sat, 6 Nov 2021 15:44:45 +0100 Subject: [PATCH 31/34] WIP fix tool deselection saga --- frontend/javascripts/oxalis/api/api_latest.js | 3 ++- frontend/javascripts/oxalis/api/api_v2.js | 3 ++- .../controller/combinations/tool_controls.js | 2 +- .../controller/viewmodes/plane_controller.js | 13 ++++++----- .../oxalis/model/actions/ui_actions.js | 17 ++++++++++---- .../oxalis/model/reducers/reducer_helpers.js | 1 - .../model/sagas/annotation_tool_saga.js | 12 +++++----- .../oxalis/model/sagas/volumetracing_saga.js | 3 ++- .../oxalis/model_initialization.js | 4 ++-- .../oxalis/view/action-bar/toolbar_view.js | 12 ++++++---- .../reducers/volumetracing_reducer.spec.js | 22 +++++++++---------- .../volumetracing_saga_integration.spec.js | 16 +++++++------- 12 files changed, 63 insertions(+), 45 deletions(-) diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index 123516bb3e5..b7d3b35207c 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -866,7 +866,8 @@ class TracingApi { `Annotation tool has to be one of: "${Object.keys(AnnotationToolEnum).join('", "')}".`, ); } - Store.dispatch(setToolAction(tool)); + const previousTool = Store.getState().uiInformation.activeTool; + Store.dispatch(setToolAction(tool, previousTool)); } /** diff --git a/frontend/javascripts/oxalis/api/api_v2.js b/frontend/javascripts/oxalis/api/api_v2.js index a901425aaf9..38ddb0b5f00 100644 --- a/frontend/javascripts/oxalis/api/api_v2.js +++ b/frontend/javascripts/oxalis/api/api_v2.js @@ -514,7 +514,8 @@ class TracingApi { `Volume tool has to be one of: "${Object.keys(AnnotationToolEnum).join('", "')}".`, ); } - Store.dispatch(setToolAction(tool)); + const previousTool = Store.getState().uiInformation.activeTool; + Store.dispatch(setToolAction(tool, previousTool)); } /** diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js index 38810885623..3450d1b5810 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.js +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.js @@ -579,8 +579,8 @@ export class BoundingBoxTool { }, mouseMove: (delta: Point2, position: Point2, _id, event: MouseEvent) => { - MoveHandlers.moveWhenAltIsPressed(delta, position, _id, event); if (primarySelectedEdge == null && planeId !== OrthoViews.TDView) { + MoveHandlers.moveWhenAltIsPressed(delta, position, _id, event); highlightAndSetCursorOnHoveredBoundingBox(delta, position, planeId); } }, diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js index ddd149356dc..0cedf099ea3 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js @@ -77,6 +77,11 @@ function ensureNonConflictingHandlers(skeletonControls: Object, volumeControls: type OwnProps = {| showContextMenuAt: ShowContextMenuFunction |}; +const cycleTools = () => { + const { activeTool } = Store.getState().uiInformation; + Store.dispatch(cycleToolAction(activeTool)); +}; + type StateProps = {| tracing: Tracing, activeTool: AnnotationTool, @@ -129,9 +134,7 @@ class VolumeKeybindings { static getKeyboardControls() { return { c: () => Store.dispatch(createCellAction()), - "1": () => { - Store.dispatch(cycleToolAction()); - }, + "1": cycleTools, v: () => { Store.dispatch(copySegmentationLayerAction()); }, @@ -376,9 +379,7 @@ class PlaneController extends React.PureComponent { h: () => this.changeMoveValue(25), g: () => this.changeMoveValue(-25), - w: () => { - Store.dispatch(cycleToolAction()); - }, + w: cycleTools, ...loopedKeyboardControls, }, { diff --git a/frontend/javascripts/oxalis/model/actions/ui_actions.js b/frontend/javascripts/oxalis/model/actions/ui_actions.js index 59f7e21c868..422039cf916 100644 --- a/frontend/javascripts/oxalis/model/actions/ui_actions.js +++ b/frontend/javascripts/oxalis/model/actions/ui_actions.js @@ -3,9 +3,13 @@ import type { AnnotationTool } from "oxalis/constants"; import { type BorderOpenStatus, type Theme } from "oxalis/store"; -export type SetToolAction = { type: "SET_TOOL", tool: AnnotationTool }; +export type SetToolAction = { + type: "SET_TOOL", + tool: AnnotationTool, + previousTool: AnnotationTool, +}; -export type CycleToolAction = { type: "CYCLE_TOOL" }; +export type CycleToolAction = { type: "CYCLE_TOOL", previousTool: AnnotationTool }; type SetDropzoneModalVisibilityAction = { type: "SET_DROPZONE_MODAL_VISIBILITY", @@ -115,13 +119,18 @@ export const setHasOrganizationsAction = (value: boolean): SetHasOrganizationsAc value, }); -export const setToolAction = (tool: AnnotationTool): SetToolAction => ({ +export const setToolAction = ( + tool: AnnotationTool, + previousTool: AnnotationTool, +): SetToolAction => ({ type: "SET_TOOL", tool, + previousTool, }); -export const cycleToolAction = (): CycleToolAction => ({ +export const cycleToolAction = (previousTool: AnnotationTool): CycleToolAction => ({ type: "CYCLE_TOOL", + previousTool, }); export const setThemeAction = (value: Theme): SetThemeAction => ({ diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js index d1601d17225..79f6d936f5e 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -142,7 +142,6 @@ export function getNextTool(state: OxalisState): AnnotationTool | null { const tools = Object.keys(AnnotationToolEnum); const currentToolIndex = tools.indexOf(state.uiInformation.activeTool); - // Search for the next tool which is not disabled. for ( let newToolIndex = currentToolIndex + 1; diff --git a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js index 22d364a4711..f83ab951a42 100644 --- a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js @@ -1,12 +1,16 @@ // @flow /* eslint-disable import/prefer-default-export */ -import { type Saga, _takeEvery, select } from "oxalis/model/sagas/effect-generators"; +import { type Saga, select, take } from "oxalis/model/sagas/effect-generators"; import type { SetToolAction, CycleToolAction } from "oxalis/model/actions/ui_actions"; import { getNextTool } from "oxalis/model/reducers/reducer_helpers"; import { getToolClassForAnnotationTool } from "oxalis/controller/combinations/tool_controls"; export function* watchToolDeselection(): Saga { - function* triggerToolDeselection(action: SetToolAction | CycleToolAction): Saga { + yield* take("WK_READY"); + while (true) { + const action = ((yield* take(["SET_TOOL", "CYCLE_TOOL"]): any): + | SetToolAction + | CycleToolAction); const storeState = yield* select(state => state); let executeDeselect = false; if (action.type === "SET_TOOL") { @@ -15,9 +19,7 @@ export function* watchToolDeselection(): Saga { executeDeselect = true; } if (executeDeselect) { - getToolClassForAnnotationTool(storeState.uiInformation.activeTool).onToolDeselected(); + getToolClassForAnnotationTool(action.previousTool).onToolDeselected(); } } - - yield _takeEvery(["CYCLE_TOOL", "SET_TOOL"], triggerToolDeselection); } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index b70d0b5f183..9af7e7c9b5a 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -767,7 +767,8 @@ export function* ensureToolIsAllowedInResolution(): Saga<*> { return isVolumeAnnotationDisallowedForZoom(activeTool, state); }); if (isResolutionTooLow) { - yield* put(setToolAction(AnnotationToolEnum.MOVE)); + const previousTool = yield* select(state => state.uiInformation.activeTool); + yield* put(setToolAction(AnnotationToolEnum.MOVE, previousTool)); } } } diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 38f828e6d18..47bb331ce69 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -74,7 +74,7 @@ import UrlManager, { type UrlStateByLayer, } from "oxalis/controller/url_manager"; import * as Utils from "libs/utils"; -import constants, { ControlModeEnum } from "oxalis/constants"; +import constants, { ControlModeEnum, AnnotationToolEnum } from "oxalis/constants"; import messages from "messages"; import window from "libs/window"; @@ -313,7 +313,7 @@ function setInitialTool() { // We are in a annotation which contains a skeleton. Due to the // enabled legacy-bindings, the user can expect to immediately create new nodes // with right click. Therefore, switch to the skeleton tool. - Store.dispatch(setToolAction("SKELETON")); + Store.dispatch(setToolAction(AnnotationToolEnum.SKELETON, AnnotationToolEnum.MOVE)); } } diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js index e84547e24ae..7becc354847 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js @@ -92,8 +92,11 @@ const handleUpdateBrushSize = (value: number) => { Store.dispatch(updateUserSettingAction("brushSize", value)); }; -const handleSetTool = (event: { target: { value: AnnotationTool } }) => { - Store.dispatch(setToolAction(event.target.value)); +const handleSetTool = ( + event: { target: { value: AnnotationTool } }, + previousSelectedTool: AnnotationTool, +) => { + Store.dispatch(setToolAction(event.target.value, previousSelectedTool)); }; const handleCreateCell = () => { @@ -384,7 +387,8 @@ export default function ToolbarView() { const disabledInfoForCurrentTool = disabledInfosForTools[activeTool]; useEffect(() => { if (disabledInfoForCurrentTool.isDisabled) { - Store.dispatch(setToolAction(AnnotationToolEnum.MOVE)); + const previousTool = activeTool; + Store.dispatch(setToolAction(AnnotationToolEnum.MOVE, previousTool)); } }, [activeTool, disabledInfoForCurrentTool]); @@ -422,7 +426,7 @@ export default function ToolbarView() { if (document.activeElement) document.activeElement.blur(); }} > - + handleSetTool(evt, activeTool)} value={adaptedActiveTool}> { - const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE); + const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE, AnnotationToolEnum.MOVE); // Change tool to Trace const newState = UiReducer(initialState, setToolAction); @@ -217,7 +217,7 @@ test("VolumeTracing should set trace/view tool", t => { }); test("VolumeTracing should not allow to set trace tool if getRequestLogZoomStep(zoomStep) is > 1", t => { - const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE); + const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE, AnnotationToolEnum.MOVE); const alteredState = update(initialState, { flycam: { zoomStep: { $set: 3 }, @@ -236,33 +236,33 @@ test("VolumeTracing should not allow to set trace tool if getRequestLogZoomStep( }); test("VolumeTracing should cycle trace/view/brush tool", t => { - const cycleToolAction = UiActions.cycleToolAction(); + const cycleToolAction = previousTool => UiActions.cycleToolAction(previousTool); // Cycle tool to Brush - let newState = UiReducer(initialState, cycleToolAction); + let newState = UiReducer(initialState, cycleToolAction(AnnotationToolEnum.MOVE)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.BRUSH); - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.BRUSH)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.ERASE_BRUSH); // Cycle tool to Trace - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.ERASE_BRUSH)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.TRACE); - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.TRACE)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.ERASE_TRACE); - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.ERASE_TRACE)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.FILL_CELL); - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.FILL_CELL)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.PICK_CELL); - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.PICK_CELL)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.BOUNDING_BOX); // Cycle tool back to MOVE - newState = UiReducer(newState, cycleToolAction); + newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.BOUNDING_BOX)); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.MOVE); }); diff --git a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js index b8704182d65..06021d74836 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js @@ -12,7 +12,7 @@ import { createBucketResponseFunction, getVolumeTracingOrFail, } from "test/helpers/apiHelpers"; -import { OrthoViews, FillModeEnum } from "oxalis/constants"; +import { OrthoViews, FillModeEnum, AnnotationToolEnum } from "oxalis/constants"; import { restartSagaAction, wkReadyAction } from "oxalis/model/actions/actions"; import Store from "oxalis/store"; import { updateUserSettingAction } from "oxalis/model/actions/settings_actions"; @@ -62,7 +62,7 @@ test.serial("Executing a floodfill in mag 1", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 43])); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -147,7 +147,7 @@ test.serial("Executing a floodfill in mag 2", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 43])); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -320,7 +320,7 @@ test.serial("Brushing/Tracing with a new segment id should update the bucket dat Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -381,7 +381,7 @@ test.serial("Brushing/Tracing with already existing backend data", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -429,7 +429,7 @@ test.serial("Brushing/Tracing with undo (I)", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); @@ -466,7 +466,7 @@ test.serial("Brushing/Tracing with undo (II)", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); @@ -561,7 +561,7 @@ async function testLabelingManyBuckets(t, saveInbetween) { ]); Store.dispatch(updateUserSettingAction("brushSize", brushSize)); - Store.dispatch(setToolAction("BRUSH")); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); Store.dispatch(setActiveCellAction(newCellId)); for (const paintPosition of paintPositions1) { From 02496cdf4e674094a25edcd56439005492e0f9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Sat, 6 Nov 2021 21:53:02 +0100 Subject: [PATCH 32/34] add tests to test annotation tool deselection --- .../test/fixtures/volumetracing_object.js | 91 ++++++++++ .../reducers/volumetracing_reducer.spec.js | 86 +--------- .../test/sagas/annotation_tool_saga.spec.js | 116 +++++++++++++ package.json | 2 +- yarn.lock | 161 +++++++++--------- 5 files changed, 287 insertions(+), 169 deletions(-) create mode 100644 frontend/javascripts/test/fixtures/volumetracing_object.js create mode 100644 frontend/javascripts/test/sagas/annotation_tool_saga.spec.js diff --git a/frontend/javascripts/test/fixtures/volumetracing_object.js b/frontend/javascripts/test/fixtures/volumetracing_object.js new file mode 100644 index 00000000000..a56399aa941 --- /dev/null +++ b/frontend/javascripts/test/fixtures/volumetracing_object.js @@ -0,0 +1,91 @@ +/* eslint-disable import/prefer-default-export */ +// @flow +import update from "immutability-helper"; + +import Constants, { AnnotationToolEnum } from "oxalis/constants"; +import mockRequire from "mock-require"; +import defaultState from "oxalis/default_state"; + +mockRequire("app", { currentUser: { firstName: "SCM", lastName: "Boy" } }); + +const volumeTracing = { + type: "volume", + activeCellId: 0, + activeTool: AnnotationToolEnum.MOVE, + maxCellId: 0, + contourList: [], + lastCentroid: null, +}; + +const notEmptyViewportRect = { + top: 0, + left: 0, + width: Constants.VIEWPORT_WIDTH, + height: Constants.VIEWPORT_WIDTH, +}; + +export const initialState = update(defaultState, { + tracing: { + annotationType: { $set: "Explorational" }, + name: { $set: "" }, + restrictions: { + $set: { + branchPointsAllowed: true, + allowUpdate: true, + allowFinish: true, + allowAccess: true, + allowDownload: true, + resolutionRestrictions: { min: null, max: null }, + }, + }, + volume: { $set: volumeTracing }, + }, + dataset: { + dataSource: { + dataLayers: { + $set: [ + { + // We need to have some resolutions. Otherwise, + // getRequestLogZoomStep will always return 0 + resolutions: [[1, 1, 1], [2, 2, 2], [4, 4, 4]], + category: "segmentation", + name: "segmentation", + isTracingLayer: true, + }, + ], + }, + }, + }, + datasetConfiguration: { + layers: { + segmentation: { + $set: { + color: [0, 0, 0], + alpha: 100, + intensityRange: [0, 255], + isDisabled: false, + isInverted: false, + isInEditMode: false, + }, + }, + }, + }, + // To get a valid calculated current zoomstep, the viewport rects are required not to be empty. + viewModeData: { + plane: { + inputCatcherRects: { + $set: { + PLANE_XY: notEmptyViewportRect, + PLANE_YZ: notEmptyViewportRect, + PLANE_XZ: notEmptyViewportRect, + TDView: notEmptyViewportRect, + }, + }, + }, + arbitrary: { + $set: { + inputCatcherRect: notEmptyViewportRect, + }, + }, + }, +}); diff --git a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js index 5cf71ea5579..690e678980c 100644 --- a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js +++ b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js @@ -2,7 +2,7 @@ import update from "immutability-helper"; import { getVolumeTracingOrFail } from "test/helpers/apiHelpers"; -import Constants, { AnnotationToolEnum } from "oxalis/constants"; +import { AnnotationToolEnum } from "oxalis/constants"; import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; import * as VolumeTracingActions from "oxalis/model/actions/volumetracing_actions"; import * as UiActions from "oxalis/model/actions/ui_actions"; @@ -10,92 +10,10 @@ import VolumeTracingReducer from "oxalis/model/reducers/volumetracing_reducer"; import UiReducer from "oxalis/model/reducers/ui_reducer"; import mockRequire from "mock-require"; import test from "ava"; -import defaultState from "oxalis/default_state"; +import { initialState } from "test/fixtures/volumetracing_object"; mockRequire("app", { currentUser: { firstName: "SCM", lastName: "Boy" } }); -const volumeTracing = { - type: "volume", - activeCellId: 0, - activeTool: AnnotationToolEnum.MOVE, - maxCellId: 0, - contourList: [], - lastCentroid: null, -}; - -const notEmptyViewportRect = { - top: 0, - left: 0, - width: Constants.VIEWPORT_WIDTH, - height: Constants.VIEWPORT_WIDTH, -}; - -const initialState = update(defaultState, { - tracing: { - annotationType: { $set: "Explorational" }, - name: { $set: "" }, - restrictions: { - $set: { - branchPointsAllowed: true, - allowUpdate: true, - allowFinish: true, - allowAccess: true, - allowDownload: true, - resolutionRestrictions: { min: null, max: null }, - }, - }, - volume: { $set: volumeTracing }, - }, - dataset: { - dataSource: { - dataLayers: { - $set: [ - { - // We need to have some resolutions. Otherwise, - // getRequestLogZoomStep will always return 0 - resolutions: [[1, 1, 1], [2, 2, 2], [4, 4, 4]], - category: "segmentation", - name: "segmentation", - isTracingLayer: true, - }, - ], - }, - }, - }, - datasetConfiguration: { - layers: { - segmentation: { - $set: { - color: [0, 0, 0], - alpha: 100, - intensityRange: [0, 255], - isDisabled: false, - isInverted: false, - isInEditMode: false, - }, - }, - }, - }, - // To get a valid calculated current zoomstep, the viewport rects are required not to be empty. - viewModeData: { - plane: { - inputCatcherRects: { - $set: { - PLANE_XY: notEmptyViewportRect, - PLANE_YZ: notEmptyViewportRect, - PLANE_XZ: notEmptyViewportRect, - TDView: notEmptyViewportRect, - }, - }, - }, - arbitrary: { - $set: { - inputCatcherRect: notEmptyViewportRect, - }, - }, - }, -}); - test("VolumeTracing should set a new active cell", t => { const createCellAction = VolumeTracingActions.createCellAction(); const setActiveCellAction = VolumeTracingActions.setActiveCellAction(1); diff --git a/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js b/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js new file mode 100644 index 00000000000..459ddf151e3 --- /dev/null +++ b/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js @@ -0,0 +1,116 @@ +// @flow + +import test from "ava"; +import { AnnotationToolEnum } from "oxalis/constants"; + +import mockRequire from "mock-require"; +import { initialState } from "test/fixtures/volumetracing_object"; +import sinon from "sinon"; + +const disabledInfoMock: { [any]: any } = {}; +Object.values(AnnotationToolEnum).forEach(annotationTool => { + disabledInfoMock[annotationTool] = { isDisabled: false, explanation: "" }; +}); +mockRequire("oxalis/model/accessors/tool_accessor", { + getDisabledInfoForTools: () => disabledInfoMock, +}); +const { + MoveTool, + SkeletonTool, + BoundingBoxTool, + DrawTool, + EraseTool, + FillCellTool, + PickCellTool, +} = mockRequire.reRequire("oxalis/controller/combinations/tool_controls"); +const UiReducer = mockRequire.reRequire("oxalis/model/reducers/ui_reducer").default; +const { wkReadyAction } = mockRequire.reRequire("oxalis/model/actions/actions"); +const { cycleToolAction, setToolAction } = mockRequire.reRequire("oxalis/model/actions/ui_actions"); + +const { watchToolDeselection } = mockRequire.reRequire("oxalis/model/sagas/annotation_tool_saga"); + +const allTools = [ + MoveTool, + SkeletonTool, + BoundingBoxTool, + DrawTool, + EraseTool, + FillCellTool, + PickCellTool, +]; + +const spies = allTools.map(tool => sinon.spy(tool, "onToolDeselected")); +test.beforeEach(() => { + spies.forEach(spy => spy.resetHistory()); +}); + +test.serial( + "Cycling through the annotation tools should trigger a deselection of the previous tool.", + t => { + let newState = initialState; + const saga = watchToolDeselection(); + saga.next(); + saga.next(wkReadyAction()); + const cycleTool = () => { + const action = cycleToolAction(newState.uiInformation.activeTool); + newState = UiReducer(newState, action); + saga.next(action); + saga.next(newState); + }; + + cycleTool(); + t.true(MoveTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(SkeletonTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(DrawTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(EraseTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(DrawTool.onToolDeselected.calledTwice); + cycleTool(); + t.true(EraseTool.onToolDeselected.calledTwice); + cycleTool(); + t.true(FillCellTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(PickCellTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(BoundingBoxTool.onToolDeselected.calledOnce); + cycleTool(); + t.true(MoveTool.onToolDeselected.calledTwice); + }, +); + +test.serial("Selecting another tool should trigger a deselection of the previous tool.", t => { + let newState = initialState; + const saga = watchToolDeselection(); + saga.next(); + saga.next(wkReadyAction()); + const cycleTool = nextTool => { + const action = setToolAction(nextTool, newState.uiInformation.activeTool); + newState = UiReducer(newState, action); + saga.next(action); + saga.next(newState); + }; + + cycleTool(AnnotationToolEnum.SKELETON); + t.true(MoveTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.BRUSH); + t.true(SkeletonTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.ERASE_BRUSH); + t.true(DrawTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.TRACE); + t.true(EraseTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.ERASE_TRACE); + t.true(DrawTool.onToolDeselected.calledTwice); + cycleTool(AnnotationToolEnum.FILL_CELL); + t.true(EraseTool.onToolDeselected.calledTwice); + cycleTool(AnnotationToolEnum.PICK_CELL); + t.true(FillCellTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.BOUNDING_BOX); + t.true(PickCellTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.MOVE); + t.true(BoundingBoxTool.onToolDeselected.calledOnce); + cycleTool(AnnotationToolEnum.SKELETON); + t.true(MoveTool.onToolDeselected.calledTwice); +}); diff --git a/package.json b/package.json index 5b292281b2a..b6c24e3a5f2 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "retoggle": "^0.3.0", "rimraf": "^2.6.2", "shelljs": "^0.8.2", - "sinon": "^1.17.3", + "sinon": "^12.0.1", "style-loader": "^0.20.2", "terser-webpack-plugin": "^1.1.0", "tmp": "0.0.33", diff --git a/yarn.lock b/yarn.lock index d3a2891fc17..f83aa4b5c92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1341,6 +1341,41 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^7.0.4": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5" + integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/fake-timers@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/samsam@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.0.2.tgz#a0117d823260f282c04bff5f8704bdc2ac6910bb" + integrity sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -2320,13 +2355,6 @@ ava@^3.13.0: write-file-atomic "^3.0.3" yargs "^16.2.0" -available-typed-arrays@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" - integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== - dependencies: - array-filter "^1.0.0" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -4184,6 +4212,11 @@ diff@^1.3.2: resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -5664,11 +5697,6 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - foreground-child@^1.5.6: version "1.5.6" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" @@ -5700,13 +5728,6 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formatio@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" - integrity sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek= - dependencies: - samsam "~1.1" - forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -7048,11 +7069,6 @@ is-function@^1.0.1: resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== -is-generator-function@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b" - integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ== - is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -7252,17 +7268,6 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.1" -is-typed-array@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.4.tgz#1f66f34a283a3c94a4335434661ca53fff801120" - integrity sha512-ILaRgn4zaSrVNXNGtON6iFNotXW3hAPF3+0fB1usg2jFlWqo5fEDdmJkz0zBfoi7Dgskr8Khi2xZ8cXqZEfXNA== - dependencies: - available-typed-arrays "^1.0.2" - call-bind "^1.0.0" - es-abstract "^1.18.0-next.1" - foreach "^2.0.5" - has-symbols "^1.0.1" - is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -7667,6 +7672,11 @@ jszip@^3.5.0: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -8024,6 +8034,11 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -8109,11 +8124,6 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== -lolex@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" - integrity sha1-fD2mL/yzDw9agKJWbKJORdigHzE= - long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -8811,6 +8821,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.0.tgz#713ef3ed138252daef20ec035ab62b7a28be645c" + integrity sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^7.0.4" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -11647,16 +11668,6 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -samsam@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" - integrity sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc= - -samsam@~1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" - integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE= - sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -11922,15 +11933,17 @@ simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" -sinon@^1.17.3: - version "1.17.7" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" - integrity sha1-RUKk9JugxFwF6y6d2dID4rjv4L8= +sinon@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-12.0.1.tgz#331eef87298752e1b88a662b699f98e403c859e9" + integrity sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg== dependencies: - formatio "1.1.1" - lolex "1.3.2" - samsam "1.1.2" - util ">=0.10.3 <1" + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" "^8.1.0" + "@sinonjs/samsam" "^6.0.2" + diff "^5.0.0" + nise "^5.1.0" + supports-color "^7.2.0" slash@^1.0.0: version "1.0.0" @@ -12541,7 +12554,7 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -12952,6 +12965,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" @@ -13330,18 +13348,6 @@ util@0.10.3: dependencies: inherits "2.0.1" -"util@>=0.10.3 <1": - version "0.12.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.3.tgz#971bb0292d2cc0c892dab7c6a5d37c2bec707888" - integrity sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - safe-buffer "^5.1.2" - which-typed-array "^1.1.2" - util@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" @@ -13762,19 +13768,6 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which-typed-array@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" - integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA== - dependencies: - available-typed-arrays "^1.0.2" - call-bind "^1.0.0" - es-abstract "^1.18.0-next.1" - foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" - which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From f317fd69c428228729bd258af4ac81011bc89857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Sun, 7 Nov 2021 00:16:01 +0100 Subject: [PATCH 33/34] small fixes --- .../oxalis/controller/combinations/bounding_box_handlers.js | 2 +- frontend/javascripts/oxalis/view/context_menu.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js index ec1d917869f..c98b5460a09 100644 --- a/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js +++ b/frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.js @@ -154,7 +154,7 @@ export function getClosestHoveredBoundingBox( const { min, max } = bbox.boundingBox; const isCrossSectionOfViewportVisible = globalPosition[thirdDim] >= min[thirdDim] && globalPosition[thirdDim] < max[thirdDim]; - if (!isCrossSectionOfViewportVisible) { + if (!isCrossSectionOfViewportVisible || !bbox.isVisible) { continue; } // In getNeighbourEdgeIndexByEdgeIndex is a visualization diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index c038763f635..29296c1053e 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -352,7 +352,6 @@ function getBoundingBoxMenuOptions({ defaultValue={hoveredBBox.name} placeholder="Bounding Box Name" size="small" - onChange={setBBoxName} onPressEnter={evt => { setBBoxName(evt); hideContextMenu(); @@ -395,7 +394,6 @@ function getBoundingBoxMenuOptions({ color = ((color.map(colorPart => colorPart / 255): any): Vector3); setBoundingBoxColor(clickedBoundingBoxId, color); }} - onBlur={() => hideContextMenu} value={rgbToHex(upscaledBBoxColor)} /> From d4372a8d4635712d59106f5dd961dbebc37300b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= Date: Sun, 14 Nov 2021 16:18:45 +0100 Subject: [PATCH 34/34] Remove the param previous tool from annotation tool setting actions --- frontend/javascripts/oxalis/api/api_latest.js | 3 +-- frontend/javascripts/oxalis/api/api_v2.js | 3 +-- .../controller/viewmodes/plane_controller.js | 3 +-- .../oxalis/model/actions/ui_actions.js | 12 +++------- .../model/sagas/annotation_tool_saga.js | 4 +++- .../oxalis/model/sagas/volumetracing_saga.js | 3 +-- .../oxalis/model_initialization.js | 2 +- .../oxalis/view/action-bar/toolbar_view.js | 12 ++++------ .../view/components/setting_input_views.js | 1 - .../reducers/volumetracing_reducer.spec.js | 22 +++++++++---------- .../test/sagas/annotation_tool_saga.spec.js | 6 +++-- .../volumetracing_saga_integration.spec.js | 16 +++++++------- 12 files changed, 38 insertions(+), 49 deletions(-) diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index b7d3b35207c..123516bb3e5 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -866,8 +866,7 @@ class TracingApi { `Annotation tool has to be one of: "${Object.keys(AnnotationToolEnum).join('", "')}".`, ); } - const previousTool = Store.getState().uiInformation.activeTool; - Store.dispatch(setToolAction(tool, previousTool)); + Store.dispatch(setToolAction(tool)); } /** diff --git a/frontend/javascripts/oxalis/api/api_v2.js b/frontend/javascripts/oxalis/api/api_v2.js index 38ddb0b5f00..a901425aaf9 100644 --- a/frontend/javascripts/oxalis/api/api_v2.js +++ b/frontend/javascripts/oxalis/api/api_v2.js @@ -514,8 +514,7 @@ class TracingApi { `Volume tool has to be one of: "${Object.keys(AnnotationToolEnum).join('", "')}".`, ); } - const previousTool = Store.getState().uiInformation.activeTool; - Store.dispatch(setToolAction(tool, previousTool)); + Store.dispatch(setToolAction(tool)); } /** diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js index 0cedf099ea3..a0c9bef079a 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js @@ -78,8 +78,7 @@ function ensureNonConflictingHandlers(skeletonControls: Object, volumeControls: type OwnProps = {| showContextMenuAt: ShowContextMenuFunction |}; const cycleTools = () => { - const { activeTool } = Store.getState().uiInformation; - Store.dispatch(cycleToolAction(activeTool)); + Store.dispatch(cycleToolAction()); }; type StateProps = {| diff --git a/frontend/javascripts/oxalis/model/actions/ui_actions.js b/frontend/javascripts/oxalis/model/actions/ui_actions.js index 422039cf916..adfbcee7fa0 100644 --- a/frontend/javascripts/oxalis/model/actions/ui_actions.js +++ b/frontend/javascripts/oxalis/model/actions/ui_actions.js @@ -6,10 +6,9 @@ import { type BorderOpenStatus, type Theme } from "oxalis/store"; export type SetToolAction = { type: "SET_TOOL", tool: AnnotationTool, - previousTool: AnnotationTool, }; -export type CycleToolAction = { type: "CYCLE_TOOL", previousTool: AnnotationTool }; +export type CycleToolAction = { type: "CYCLE_TOOL" }; type SetDropzoneModalVisibilityAction = { type: "SET_DROPZONE_MODAL_VISIBILITY", @@ -119,18 +118,13 @@ export const setHasOrganizationsAction = (value: boolean): SetHasOrganizationsAc value, }); -export const setToolAction = ( - tool: AnnotationTool, - previousTool: AnnotationTool, -): SetToolAction => ({ +export const setToolAction = (tool: AnnotationTool): SetToolAction => ({ type: "SET_TOOL", tool, - previousTool, }); -export const cycleToolAction = (previousTool: AnnotationTool): CycleToolAction => ({ +export const cycleToolAction = (): CycleToolAction => ({ type: "CYCLE_TOOL", - previousTool, }); export const setThemeAction = (value: Theme): SetThemeAction => ({ diff --git a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js index f83ab951a42..204b836f412 100644 --- a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.js @@ -7,6 +7,7 @@ import { getToolClassForAnnotationTool } from "oxalis/controller/combinations/to export function* watchToolDeselection(): Saga { yield* take("WK_READY"); + let previousTool = yield* select(state => state.uiInformation.activeTool); while (true) { const action = ((yield* take(["SET_TOOL", "CYCLE_TOOL"]): any): | SetToolAction @@ -19,7 +20,8 @@ export function* watchToolDeselection(): Saga { executeDeselect = true; } if (executeDeselect) { - getToolClassForAnnotationTool(action.previousTool).onToolDeselected(); + getToolClassForAnnotationTool(previousTool).onToolDeselected(); } + previousTool = storeState.uiInformation.activeTool; } } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index 9af7e7c9b5a..b70d0b5f183 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -767,8 +767,7 @@ export function* ensureToolIsAllowedInResolution(): Saga<*> { return isVolumeAnnotationDisallowedForZoom(activeTool, state); }); if (isResolutionTooLow) { - const previousTool = yield* select(state => state.uiInformation.activeTool); - yield* put(setToolAction(AnnotationToolEnum.MOVE, previousTool)); + yield* put(setToolAction(AnnotationToolEnum.MOVE)); } } } diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 47bb331ce69..682d4c06036 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -313,7 +313,7 @@ function setInitialTool() { // We are in a annotation which contains a skeleton. Due to the // enabled legacy-bindings, the user can expect to immediately create new nodes // with right click. Therefore, switch to the skeleton tool. - Store.dispatch(setToolAction(AnnotationToolEnum.SKELETON, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.SKELETON)); } } diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js index 7becc354847..e84547e24ae 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.js @@ -92,11 +92,8 @@ const handleUpdateBrushSize = (value: number) => { Store.dispatch(updateUserSettingAction("brushSize", value)); }; -const handleSetTool = ( - event: { target: { value: AnnotationTool } }, - previousSelectedTool: AnnotationTool, -) => { - Store.dispatch(setToolAction(event.target.value, previousSelectedTool)); +const handleSetTool = (event: { target: { value: AnnotationTool } }) => { + Store.dispatch(setToolAction(event.target.value)); }; const handleCreateCell = () => { @@ -387,8 +384,7 @@ export default function ToolbarView() { const disabledInfoForCurrentTool = disabledInfosForTools[activeTool]; useEffect(() => { if (disabledInfoForCurrentTool.isDisabled) { - const previousTool = activeTool; - Store.dispatch(setToolAction(AnnotationToolEnum.MOVE, previousTool)); + Store.dispatch(setToolAction(AnnotationToolEnum.MOVE)); } }, [activeTool, disabledInfoForCurrentTool]); @@ -426,7 +422,7 @@ export default function ToolbarView() { if (document.activeElement) document.activeElement.blur(); }} > - handleSetTool(evt, activeTool)} value={adaptedActiveTool}> + { - const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE, AnnotationToolEnum.MOVE); + const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE); // Change tool to Trace const newState = UiReducer(initialState, setToolAction); @@ -135,7 +135,7 @@ test("VolumeTracing should set trace/view tool", t => { }); test("VolumeTracing should not allow to set trace tool if getRequestLogZoomStep(zoomStep) is > 1", t => { - const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE, AnnotationToolEnum.MOVE); + const setToolAction = UiActions.setToolAction(AnnotationToolEnum.TRACE); const alteredState = update(initialState, { flycam: { zoomStep: { $set: 3 }, @@ -154,33 +154,33 @@ test("VolumeTracing should not allow to set trace tool if getRequestLogZoomStep( }); test("VolumeTracing should cycle trace/view/brush tool", t => { - const cycleToolAction = previousTool => UiActions.cycleToolAction(previousTool); + const cycleToolAction = () => UiActions.cycleToolAction(); // Cycle tool to Brush - let newState = UiReducer(initialState, cycleToolAction(AnnotationToolEnum.MOVE)); + let newState = UiReducer(initialState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.BRUSH); - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.BRUSH)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.ERASE_BRUSH); // Cycle tool to Trace - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.ERASE_BRUSH)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.TRACE); - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.TRACE)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.ERASE_TRACE); - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.ERASE_TRACE)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.FILL_CELL); - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.FILL_CELL)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.PICK_CELL); - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.PICK_CELL)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.BOUNDING_BOX); // Cycle tool back to MOVE - newState = UiReducer(newState, cycleToolAction(AnnotationToolEnum.BOUNDING_BOX)); + newState = UiReducer(newState, cycleToolAction()); t.is(newState.uiInformation.activeTool, AnnotationToolEnum.MOVE); }); diff --git a/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js b/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js index 459ddf151e3..81984c08f76 100644 --- a/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js +++ b/frontend/javascripts/test/sagas/annotation_tool_saga.spec.js @@ -51,8 +51,9 @@ test.serial( const saga = watchToolDeselection(); saga.next(); saga.next(wkReadyAction()); + saga.next(newState.uiInformation.activeTool); const cycleTool = () => { - const action = cycleToolAction(newState.uiInformation.activeTool); + const action = cycleToolAction(); newState = UiReducer(newState, action); saga.next(action); saga.next(newState); @@ -86,8 +87,9 @@ test.serial("Selecting another tool should trigger a deselection of the previous const saga = watchToolDeselection(); saga.next(); saga.next(wkReadyAction()); + saga.next(newState.uiInformation.activeTool); const cycleTool = nextTool => { - const action = setToolAction(nextTool, newState.uiInformation.activeTool); + const action = setToolAction(nextTool); newState = UiReducer(newState, action); saga.next(action); saga.next(newState); diff --git a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js index 06021d74836..58dd8590332 100644 --- a/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js +++ b/frontend/javascripts/test/sagas/volumetracing_saga_integration.spec.js @@ -62,7 +62,7 @@ test.serial("Executing a floodfill in mag 1", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 43])); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -147,7 +147,7 @@ test.serial("Executing a floodfill in mag 2", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 43])); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -262,7 +262,7 @@ test.serial("Executing a floodfill in mag 1 (long operation)", async t => { // Assert state after flood-fill await assertFloodFilledState(); - // Undo created bounding box by flood fill and flood fill and assert initial state + // Undo created bounding box by flood fill and flood fill and assert initial state. await dispatchUndoAsync(Store.dispatch); await dispatchUndoAsync(Store.dispatch); await assertInitialState(); @@ -320,7 +320,7 @@ test.serial("Brushing/Tracing with a new segment id should update the bucket dat Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -381,7 +381,7 @@ test.serial("Brushing/Tracing with already existing backend data", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); Store.dispatch(addToLayerAction(paintCenter)); @@ -429,7 +429,7 @@ test.serial("Brushing/Tracing with undo (I)", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); @@ -466,7 +466,7 @@ test.serial("Brushing/Tracing with undo (II)", async t => { Store.dispatch(updateUserSettingAction("brushSize", brushSize)); Store.dispatch(setPositionAction([0, 0, 0])); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); @@ -561,7 +561,7 @@ async function testLabelingManyBuckets(t, saveInbetween) { ]); Store.dispatch(updateUserSettingAction("brushSize", brushSize)); - Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH, AnnotationToolEnum.MOVE)); + Store.dispatch(setToolAction(AnnotationToolEnum.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); for (const paintPosition of paintPositions1) {