From ea7d1119a70cb49cdd156a2bafd964ae0aba7cc3 Mon Sep 17 00:00:00 2001 From: Markus Fleischhacker Date: Fri, 10 Nov 2023 17:05:43 +0100 Subject: [PATCH] Refactor bounding shape drawing (#103) * Refactor bounding shape drawing. * Add license headers. --- .../controller/Controller.java | 53 +++-- .../ui/BoundingBoxDrawer.java | 102 ++++++++ .../ui/BoundingFreeHandShapeDrawer.java | 138 +++++++++++ .../ui/BoundingPolygonDrawer.java | 93 ++++++++ .../ui/BoundingShapeDrawer.java | 31 +++ .../ui/EditorImagePaneView.java | 225 ++++-------------- .../boundingboxeditor/ui/EditorView.java | 6 - .../controller/SceneKeyShortcutTests.java | 17 +- .../ui/BoundingPolygonDrawingTests.java | 5 +- 9 files changed, 454 insertions(+), 216 deletions(-) create mode 100644 src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingBoxDrawer.java create mode 100644 src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingFreeHandShapeDrawer.java create mode 100644 src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawer.java create mode 100644 src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingShapeDrawer.java diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java b/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java index 0a41482..35e6eba 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java @@ -201,15 +201,8 @@ public Controller(final Stage mainStage, final MainView view, final HostServices this.view = view; this.hostServices = hostServices; - stage.setTitle(PROGRAM_IDENTIFIER); - stage.getIcons().add(MainView.APPLICATION_ICON); - stage.setOnCloseRequest(event -> { - onRegisterExitAction(); - event.consume(); - }); - + setupStage(); loadPreferences(); - view.connectToController(this); setUpModelListeners(); setUpServices(); @@ -463,6 +456,8 @@ public void onRegisterAddObjectCategoryAction() { * Handles the event of the user requesting to exit the application. */ public void onRegisterExitAction() { + view.getEditorImagePane().finalizeBoundingShapeDrawing(); + updateModelFromView(); if(!model.isSaved()) { @@ -608,14 +603,10 @@ public void onRegisterImageViewMouseReleasedEvent(MouseEvent event) { view.getEditorImageView().setCursor(Cursor.OPEN_HAND); } - if(view.getObjectCategoryTable().isCategorySelected()) { - if(imagePane.getDrawingMode() == EditorImagePaneView.DrawingMode.BOX - && imagePane.isBoundingBoxDrawingInProgress()) { - imagePane.finalizeBoundingBox(); - } else if(imagePane.getDrawingMode() == EditorImagePaneView.DrawingMode.FREEHAND - && imagePane.isFreehandDrawingInProgress()) { - imagePane.finalizeFreehandShape(); - } + if(view.getObjectCategoryTable().isCategorySelected() && + (Objects.equals(imagePane.getCurrentBoundingShapeDrawingMode(), EditorImagePaneView.DrawingMode.BOX) || + Objects.equals(imagePane.getCurrentBoundingShapeDrawingMode(), EditorImagePaneView.DrawingMode.FREEHAND))) { + imagePane.finalizeBoundingShapeDrawing(); } } } @@ -632,16 +623,15 @@ public void onRegisterImageViewMousePressedEvent(MouseEvent event) { && !event.isShortcutDown() && imagePaneView.isCategorySelected()) { if(event.getButton().equals(MouseButton.PRIMARY)) { - if(imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.BOX) { - imagePaneView.initializeBoundingRectangle(event); - } else if(imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.POLYGON) { - imagePaneView.initializeBoundingPolygon(event); - } else if(imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.FREEHAND) { - imagePaneView.initializeBoundingFreehandShape(event); + if(!imagePaneView.isDrawingInProgress()) { + imagePaneView.initializeBoundingShapeDrawing(event); + } else { + imagePaneView.updateBoundingShapeDrawing(event); } } else if(event.getButton().equals(MouseButton.SECONDARY) - && imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.POLYGON) { - imagePaneView.setBoundingPolygonsEditingAndConstructing(false); + && Objects.equals(imagePaneView.getCurrentBoundingShapeDrawingMode(), + EditorImagePaneView.DrawingMode.POLYGON)) { + imagePaneView.finalizeBoundingShapeDrawing(); } } } @@ -1598,6 +1588,21 @@ private void setCurrentAnnotationLoadingDirectory(File source) { } } + private void setupStage() { + stage.setTitle(PROGRAM_IDENTIFIER); + stage.getIcons().add(MainView.APPLICATION_ICON); + stage.setOnCloseRequest(event -> { + onRegisterExitAction(); + event.consume(); + }); + + stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { + if (event.getTarget() != view.getEditorImageView()) { + view.getEditorImagePane().finalizeBoundingShapeDrawing(); + } + }); + } + /** * Class containing possible key-combinations. */ diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingBoxDrawer.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingBoxDrawer.java new file mode 100644 index 0000000..e3e0d62 --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingBoxDrawer.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 Markus Fleischhacker + * + * This file is part of Bounding Box Editor + * + * Bounding Box Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Bounding Box Editor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Bounding Box Editor. If not, see . + */ +package com.github.mfl28.boundingboxeditor.ui; + +import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory; +import com.github.mfl28.boundingboxeditor.utils.MathUtils; +import javafx.geometry.Point2D; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; + +import java.util.List; + + +public class BoundingBoxDrawer implements BoundingShapeDrawer { + private final ImageView imageView; + private final ToggleGroup toggleGroup; + private final List boundingShapes; + + private BoundingBoxView boundingBoxView; + + private boolean drawingInProgress = false; + + public BoundingBoxDrawer(ImageView imageView, ToggleGroup toggleGroup, List boundingShapes) { + this.imageView = imageView; + this.toggleGroup = toggleGroup; + this.boundingShapes = boundingShapes; + } + + @Override + public void initializeShape(MouseEvent event, ObjectCategory objectCategory) { + if (event.getEventType().equals(MouseEvent.MOUSE_PRESSED) && event.getButton().equals(MouseButton.PRIMARY)) { + Point2D parentCoordinates = imageView.localToParent(event.getX(), event.getY()); + + boundingBoxView = new BoundingBoxView(objectCategory); + boundingBoxView.getConstructionAnchorLocal().setFromMouseEvent(event); + boundingBoxView.setToggleGroup(toggleGroup); + + boundingBoxView.setX(parentCoordinates.getX()); + boundingBoxView.setY(parentCoordinates.getY()); + boundingBoxView.setWidth(0); + boundingBoxView.setHeight(0); + + boundingShapes.add(boundingBoxView); + + boundingBoxView.autoScaleWithBounds(imageView.boundsInParentProperty()); + toggleGroup.selectToggle(boundingBoxView); + + drawingInProgress = true; + } + } + + @Override + public void updateShape(MouseEvent event) { + if (event.getEventType().equals(MouseEvent.MOUSE_DRAGGED) && event.getButton().equals(MouseButton.PRIMARY)) { + final Point2D clampedEventXY = + MathUtils.clampWithinBounds(event.getX(), event.getY(), imageView.getBoundsInLocal()); + + DragAnchor constructionAnchor = boundingBoxView.getConstructionAnchorLocal(); + Point2D parentCoordinates = + imageView.localToParent(Math.min(clampedEventXY.getX(), constructionAnchor.getX()), + Math.min(clampedEventXY.getY(), constructionAnchor.getY())); + + boundingBoxView.setX(parentCoordinates.getX()); + boundingBoxView.setY(parentCoordinates.getY()); + boundingBoxView.setWidth(Math.abs(clampedEventXY.getX() - constructionAnchor.getX())); + boundingBoxView.setHeight(Math.abs(clampedEventXY.getY() - constructionAnchor.getY())); + } + } + + @Override + public void finalizeShape() { + drawingInProgress = false; + } + + @Override + public boolean isDrawingInProgress() { + return drawingInProgress; + } + + @Override + public EditorImagePaneView.DrawingMode getDrawingMode() { + return EditorImagePaneView.DrawingMode.BOX; + } +} diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingFreeHandShapeDrawer.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingFreeHandShapeDrawer.java new file mode 100644 index 0000000..77775a4 --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingFreeHandShapeDrawer.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 Markus Fleischhacker + * + * This file is part of Bounding Box Editor + * + * Bounding Box Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Bounding Box Editor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Bounding Box Editor. If not, see . + */ +package com.github.mfl28.boundingboxeditor.ui; + +import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory; +import com.github.mfl28.boundingboxeditor.utils.MathUtils; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.geometry.Point2D; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.shape.ClosePath; + +import java.util.List; + +public class BoundingFreeHandShapeDrawer implements BoundingShapeDrawer { + + private final ImageView imageView; + private final ToggleGroup toggleGroup; + private final List boundingShapes; + private final BooleanProperty autoSimplify; + private final DoubleProperty simplifyRelativeDistanceTolerance; + private boolean drawingInProgress = false; + private BoundingFreehandShapeView boundingFreehandShapeView; + + public BoundingFreeHandShapeDrawer(ImageView imageView, ToggleGroup toggleGroup, List boundingShapes, + BooleanProperty autoSimplify, DoubleProperty simplifyRelativeDistanceTolerance) { + this.imageView = imageView; + this.toggleGroup = toggleGroup; + this.boundingShapes = boundingShapes; + this.autoSimplify = autoSimplify; + this.simplifyRelativeDistanceTolerance = simplifyRelativeDistanceTolerance; + } + + @Override + public void initializeShape(MouseEvent event, ObjectCategory objectCategory) { + if (event.getEventType().equals(MouseEvent.MOUSE_PRESSED) && event.getButton().equals(MouseButton.PRIMARY)) { + Point2D parentCoordinates = imageView.localToParent(event.getX(), event.getY()); + + boundingFreehandShapeView = new BoundingFreehandShapeView(objectCategory); + boundingFreehandShapeView.setToggleGroup(toggleGroup); + + boundingShapes.add(boundingFreehandShapeView); + + boundingFreehandShapeView.autoScaleWithBounds(imageView.boundsInParentProperty()); + + boundingFreehandShapeView.setVisible(true); + toggleGroup.selectToggle(boundingFreehandShapeView); + boundingFreehandShapeView.addMoveTo(parentCoordinates.getX(), parentCoordinates.getY()); + drawingInProgress = true; + } + } + + @Override + public void updateShape(MouseEvent event) { + if (event.getEventType().equals(MouseEvent.MOUSE_DRAGGED) && event.getButton().equals(MouseButton.PRIMARY)) { + final Point2D clampedEventXY = + MathUtils.clampWithinBounds(event.getX(), event.getY(), imageView.getBoundsInLocal()); + + Point2D parentCoordinates = + imageView.localToParent(clampedEventXY.getX(), clampedEventXY.getY()); + + boundingFreehandShapeView.addLineTo(parentCoordinates.getX(), parentCoordinates.getY()); + } + } + + @Override + public void finalizeShape() { + boundingFreehandShapeView.getElements().add(new ClosePath()); + + BoundingPolygonView boundingPolygonView = new BoundingPolygonView( + boundingFreehandShapeView.getViewData().getObjectCategory()); + + final List pointsInImage = boundingFreehandShapeView.getPointsInImage(); + + boundingPolygonView.setEditing(true); + + for(int i = 0; i < pointsInImage.size(); i += 2) { + boundingPolygonView.appendNode(pointsInImage.get(i), pointsInImage.get(i + 1)); + } + + if(autoSimplify.get()) { + boundingPolygonView.simplify(simplifyRelativeDistanceTolerance.get(), + boundingFreehandShapeView.getViewData().autoScaleBounds().getValue()); + } + + boundingPolygonView.setToggleGroup(toggleGroup); + + boundingShapes.remove(boundingFreehandShapeView); + + ObjectCategoryTreeItem parentTreeItem = (ObjectCategoryTreeItem) boundingFreehandShapeView.getViewData() + .getTreeItem().getParent(); + parentTreeItem.detachBoundingShapeTreeItemChild(boundingFreehandShapeView.getViewData().getTreeItem()); + + if(parentTreeItem.getChildren().isEmpty()) { + parentTreeItem.getParent().getChildren().remove(parentTreeItem); + } + + boundingShapes.add(boundingPolygonView); + + boundingPolygonView.autoScaleWithBounds(imageView.boundsInParentProperty()); + boundingPolygonView.setVisible(true); + toggleGroup.selectToggle(boundingPolygonView); + + boundingPolygonView.setConstructing(false); + boundingPolygonView.setEditing(false); + + drawingInProgress = false; + } + + @Override + public boolean isDrawingInProgress() { + return drawingInProgress; + } + + @Override + public EditorImagePaneView.DrawingMode getDrawingMode() { + return EditorImagePaneView.DrawingMode.FREEHAND; + } +} diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawer.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawer.java new file mode 100644 index 0000000..0d36f6b --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawer.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 Markus Fleischhacker + * + * This file is part of Bounding Box Editor + * + * Bounding Box Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Bounding Box Editor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Bounding Box Editor. If not, see . + */ +package com.github.mfl28.boundingboxeditor.ui; + +import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; + +import java.util.List; + +public class BoundingPolygonDrawer implements BoundingShapeDrawer { + private final ImageView imageView; + private final ToggleGroup toggleGroup; + private final List boundingShapes; + private boolean drawingInProgress = false; + private BoundingPolygonView boundingPolygonView; + + public BoundingPolygonDrawer(ImageView imageView, ToggleGroup toggleGroup, List boundingShapes) { + + this.imageView = imageView; + this.toggleGroup = toggleGroup; + this.boundingShapes = boundingShapes; + } + + @Override + public void initializeShape(MouseEvent event, ObjectCategory objectCategory) { + if(event.getEventType().equals(MouseEvent.MOUSE_PRESSED) && event.getButton().equals(MouseButton.PRIMARY)) { + boundingShapes.forEach(boundingShapeViewable -> ((Node) boundingShapeViewable).setMouseTransparent(true)); + boundingPolygonView = new BoundingPolygonView(objectCategory); + boundingPolygonView.setToggleGroup(toggleGroup); + boundingPolygonView.setConstructing(true); + + boundingShapes.add(boundingPolygonView); + + boundingPolygonView.autoScaleWithBounds(imageView.boundsInParentProperty()); + boundingPolygonView.setMouseTransparent(true); + boundingPolygonView.setVisible(true); + toggleGroup.selectToggle(boundingPolygonView); + + updateShape(event); + } + } + + @Override + public void updateShape(MouseEvent event) { + if(event.getEventType().equals(MouseEvent.MOUSE_PRESSED) && event.getButton().equals(MouseButton.PRIMARY)) { + Point2D parentCoordinates = imageView.localToParent(event.getX(), event.getY()); + boundingPolygonView.appendNode(parentCoordinates.getX(), parentCoordinates.getY()); + boundingPolygonView.setEditing(true); + + drawingInProgress = true; + } + } + + @Override + public void finalizeShape() { + boundingPolygonView.setConstructing(false); + boundingPolygonView.setEditing(false); + + boundingShapes.forEach(boundingShapeViewable -> ((Node) boundingShapeViewable).setMouseTransparent(false)); + drawingInProgress = false; + } + + @Override + public boolean isDrawingInProgress() { + return drawingInProgress; + } + + @Override + public EditorImagePaneView.DrawingMode getDrawingMode() { + return EditorImagePaneView.DrawingMode.POLYGON; + } +} diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingShapeDrawer.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingShapeDrawer.java new file mode 100644 index 0000000..da6c4ba --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingShapeDrawer.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Markus Fleischhacker + * + * This file is part of Bounding Box Editor + * + * Bounding Box Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Bounding Box Editor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Bounding Box Editor. If not, see . + */ +package com.github.mfl28.boundingboxeditor.ui; + +import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory; +import javafx.scene.input.MouseEvent; + +public interface BoundingShapeDrawer { + void initializeShape(MouseEvent event, ObjectCategory objectCategory); + void updateShape(MouseEvent event); + void finalizeShape(); + boolean isDrawingInProgress(); + + EditorImagePaneView.DrawingMode getDrawingMode(); +} diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorImagePaneView.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorImagePaneView.java index 97ffdaf..a2b73d2 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorImagePaneView.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorImagePaneView.java @@ -21,7 +21,6 @@ import com.github.mfl28.boundingboxeditor.controller.Controller; import com.github.mfl28.boundingboxeditor.model.data.ImageMetaData; import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory; -import com.github.mfl28.boundingboxeditor.utils.MathUtils; import javafx.beans.Observable; import javafx.beans.property.*; import javafx.collections.FXCollections; @@ -33,7 +32,6 @@ import javafx.scene.Group; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; -import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.effect.ColorAdjust; import javafx.scene.image.Image; @@ -41,10 +39,8 @@ import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; -import javafx.scene.shape.ClosePath; import java.util.Collection; -import java.util.List; /** * A UI-element responsible for displaying the currently selected image on which the @@ -77,12 +73,11 @@ public class EditorImagePaneView extends ScrollPane implements View { private final ProgressIndicator imageLoadingProgressIndicator = new ProgressIndicator(); private final StackPane contentPane = new StackPane(imageView, boundingShapeSceneGroup, imageLoadingProgressIndicator); - private boolean boundingBoxDrawingInProgress = false; - private DrawingMode drawingMode = DrawingMode.BOX; - - private boolean freehandDrawingInProgress = false; + private final ObjectProperty drawingMode = new SimpleObjectProperty<>(DrawingMode.BOX); private String currentImageUrl = null; + private BoundingShapeDrawer boundingShapeDrawer = null; + /** * Creates a new image-pane UI-element responsible for displaying the currently selected image on which the * user can draw bounding-shapes. @@ -109,12 +104,8 @@ public void connectToController(Controller controller) { imageView.setOnMousePressed(controller::onRegisterImageViewMousePressedEvent); } - public DrawingMode getDrawingMode() { - return drawingMode; - } - public void setDrawingMode(DrawingMode drawingMode) { - this.drawingMode = drawingMode; + this.drawingMode.set(drawingMode); } public DoubleProperty simplifyRelativeDistanceToleranceProperty() { @@ -125,11 +116,41 @@ public BooleanProperty autoSimplifyPolygonsProperty() { return autoSimplifyPolygons; } - /** - * Finalizes the currently drawn {@link BoundingBoxView}. - */ - public void finalizeBoundingBox() { - boundingBoxDrawingInProgress = false; + public void initializeBoundingShapeDrawing(MouseEvent event) { + if (isCategorySelected()) { + boundingShapeDrawer = switch (drawingMode.get()) { + case BOX -> new BoundingBoxDrawer(imageView, boundingShapeSelectionGroup, currentBoundingShapes); + case POLYGON -> new BoundingPolygonDrawer(imageView, boundingShapeSelectionGroup, currentBoundingShapes); + case FREEHAND -> new BoundingFreeHandShapeDrawer(imageView, boundingShapeSelectionGroup, + currentBoundingShapes, autoSimplifyPolygons, + simplifyRelativeDistanceTolerance); + default -> null; + }; + + if (boundingShapeDrawer != null) { + boundingShapeDrawer.initializeShape(event, selectedCategory.get()); + } + } + } + + public void updateBoundingShapeDrawing(MouseEvent event) { + if (boundingShapeDrawer != null && boundingShapeDrawer.isDrawingInProgress()) { + boundingShapeDrawer.updateShape(event); + } + } + + public void finalizeBoundingShapeDrawing() { + if (boundingShapeDrawer != null && boundingShapeDrawer.isDrawingInProgress()) { + boundingShapeDrawer.finalizeShape(); + } + } + + public DrawingMode getCurrentBoundingShapeDrawingMode() { + if (boundingShapeDrawer == null || !boundingShapeDrawer.isDrawingInProgress()) { + return DrawingMode.NONE; + } + + return boundingShapeDrawer.getDrawingMode(); } /** @@ -191,138 +212,8 @@ public boolean isImageFullyLoaded() { return image != null && image.getProgress() == 1.0; } - /** - * Returns a boolean indicating that a bounding box is currently drawn by the user. - * - * @return the boolean value - */ - public boolean isBoundingBoxDrawingInProgress() { - return boundingBoxDrawingInProgress; - } - - public boolean isFreehandDrawingInProgress() { - return freehandDrawingInProgress; - } - public boolean isDrawingInProgress() { - return boundingBoxDrawingInProgress || freehandDrawingInProgress; - } - - public void initializeBoundingRectangle(MouseEvent event) { - Point2D parentCoordinates = imageView.localToParent(event.getX(), event.getY()); - - BoundingBoxView boundingBoxView = new BoundingBoxView(selectedCategory.get()); - boundingBoxView.getConstructionAnchorLocal().setFromMouseEvent(event); - boundingBoxView.setToggleGroup(boundingShapeSelectionGroup); - - boundingBoxView.setX(parentCoordinates.getX()); - boundingBoxView.setY(parentCoordinates.getY()); - boundingBoxView.setWidth(0); - boundingBoxView.setHeight(0); - - currentBoundingShapes.add(boundingBoxView); - - boundingBoxView.autoScaleWithBounds(imageView.boundsInParentProperty()); - boundingShapeSelectionGroup.selectToggle(boundingBoxView); - - boundingBoxDrawingInProgress = true; - } - - public void initializeBoundingPolygon(MouseEvent event) { - Toggle selectedBoundingShape = boundingShapeSelectionGroup.getSelectedToggle(); - - BoundingPolygonView selectedBoundingPolygon; - - if(!(selectedBoundingShape instanceof BoundingPolygonView boundingPolygonView && - boundingPolygonView.isConstructing())) { - selectedBoundingPolygon = new BoundingPolygonView(selectedCategory.get()); - selectedBoundingPolygon.setToggleGroup(boundingShapeSelectionGroup); - selectedBoundingPolygon.setConstructing(true); - - currentBoundingShapes.add(selectedBoundingPolygon); - - selectedBoundingPolygon.autoScaleWithBounds(imageView.boundsInParentProperty()); - selectedBoundingPolygon.setMouseTransparent(true); - selectedBoundingPolygon.setVisible(true); - boundingShapeSelectionGroup.selectToggle(selectedBoundingPolygon); - } else { - selectedBoundingPolygon = (BoundingPolygonView) selectedBoundingShape; - } - - Point2D parentCoordinates = imageView.localToParent(event.getX(), event.getY()); - selectedBoundingPolygon.appendNode(parentCoordinates.getX(), parentCoordinates.getY()); - selectedBoundingPolygon.setEditing(true); - } - - public void initializeBoundingFreehandShape(MouseEvent event) { - Point2D parentCoordinates = imageView.localToParent(event.getX(), event.getY()); - - BoundingFreehandShapeView boundingFreehandShape = new BoundingFreehandShapeView(selectedCategory.get()); - boundingFreehandShape.setToggleGroup(boundingShapeSelectionGroup); - - currentBoundingShapes.add(boundingFreehandShape); - - - boundingFreehandShape.autoScaleWithBounds(imageView.boundsInParentProperty()); - - boundingFreehandShape.setVisible(true); - boundingShapeSelectionGroup.selectToggle(boundingFreehandShape); - boundingFreehandShape.addMoveTo(parentCoordinates.getX(), parentCoordinates.getY()); - freehandDrawingInProgress = true; - } - - public void finalizeFreehandShape() { - BoundingFreehandShapeView boundingFreehandShape = (BoundingFreehandShapeView) boundingShapeSelectionGroup - .getSelectedToggle(); - - boundingFreehandShape.getElements().add(new ClosePath()); - - BoundingPolygonView boundingPolygonView = new BoundingPolygonView( - boundingFreehandShape.getViewData().getObjectCategory()); - - final List pointsInImage = boundingFreehandShape.getPointsInImage(); - - boundingPolygonView.setEditing(true); - - for(int i = 0; i < pointsInImage.size(); i += 2) { - boundingPolygonView.appendNode(pointsInImage.get(i), pointsInImage.get(i + 1)); - } - - if(autoSimplifyPolygons.get()) { - boundingPolygonView.simplify(simplifyRelativeDistanceTolerance.get(), - boundingFreehandShape.getViewData().autoScaleBounds().getValue()); - } - - boundingPolygonView.setToggleGroup(boundingShapeSelectionGroup); - - currentBoundingShapes.remove(boundingFreehandShape); - - ObjectCategoryTreeItem parentTreeItem = (ObjectCategoryTreeItem) boundingFreehandShape.getViewData() - .getTreeItem().getParent(); - parentTreeItem.detachBoundingShapeTreeItemChild(boundingFreehandShape.getViewData().getTreeItem()); - - if(parentTreeItem.getChildren().isEmpty()) { - parentTreeItem.getParent().getChildren().remove(parentTreeItem); - } - - currentBoundingShapes.add(boundingPolygonView); - - boundingPolygonView.autoScaleWithBounds(imageView.boundsInParentProperty()); - boundingPolygonView.setVisible(true); - boundingShapeSelectionGroup.selectToggle(boundingPolygonView); - setBoundingPolygonsEditingAndConstructing(false); - - freehandDrawingInProgress = false; - } - - public void setBoundingPolygonsEditingAndConstructing(boolean editing) { - currentBoundingShapes.stream() - .filter(BoundingPolygonView.class::isInstance) - .map(BoundingPolygonView.class::cast) - .forEach(boundingPolygonView -> { - boundingPolygonView.setEditing(editing); - boundingPolygonView.setConstructing(false); - }); + return (boundingShapeDrawer != null && boundingShapeDrawer.isDrawingInProgress()); } public boolean isCategorySelected() { @@ -505,44 +396,12 @@ private void setUpImageViewListeners() { if(isDrawingInProgress() && event.getButton().equals(MouseButton.PRIMARY) && isCategorySelected()) { - final Point2D clampedEventXY = - MathUtils.clampWithinBounds(event.getX(), event.getY(), imageView.getBoundsInLocal()); - - if(drawingMode == DrawingMode.BOX) { - updateCurrentBoundingBoxFromMouseDrag(clampedEventXY); - } else if(drawingMode == DrawingMode.FREEHAND) { - updateCurrentFreehandShapeFromMouseDrag(clampedEventXY); - } + boundingShapeDrawer.updateShape(event); } } }); } - private void updateCurrentFreehandShapeFromMouseDrag(Point2D clampedEventXY) { - Point2D parentCoordinates = - imageView.localToParent(clampedEventXY.getX(), clampedEventXY.getY()); - - BoundingFreehandShapeView shape = - (BoundingFreehandShapeView) boundingShapeSelectionGroup.getSelectedToggle(); - - shape.addLineTo(parentCoordinates.getX(), parentCoordinates.getY()); - } - - private void updateCurrentBoundingBoxFromMouseDrag(Point2D clampedEventXY) { - BoundingBoxView boundingBoxView = - (BoundingBoxView) boundingShapeSelectionGroup.getSelectedToggle(); - - DragAnchor constructionAnchor = boundingBoxView.getConstructionAnchorLocal(); - Point2D parentCoordinates = - imageView.localToParent(Math.min(clampedEventXY.getX(), constructionAnchor.getX()), - Math.min(clampedEventXY.getY(), constructionAnchor.getY())); - - boundingBoxView.setX(parentCoordinates.getX()); - boundingBoxView.setY(parentCoordinates.getY()); - boundingBoxView.setWidth(Math.abs(clampedEventXY.getX() - constructionAnchor.getX())); - boundingBoxView.setHeight(Math.abs(clampedEventXY.getY() - constructionAnchor.getY())); - } - private void setUpContentPaneListeners() { contentPane.setOnScroll(event -> { if(isImageFullyLoaded() && event.isShortcutDown()) { @@ -616,5 +475,5 @@ private Dimension2D calculateLoadedImageDimensions(double width, double height) } } - public enum DrawingMode {BOX, POLYGON, FREEHAND} + public enum DrawingMode {BOX, POLYGON, FREEHAND, NONE} } diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorView.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorView.java index 64ef750..6e28c85 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorView.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/EditorView.java @@ -86,24 +86,18 @@ private void setUpInternalListeners() { .setOnAction(event -> editorImagePaneView.resetImageViewSize()); editorToolBarView.getRectangleModeButton().selectedProperty().addListener((observable, oldValue, newValue) -> { - editorImagePaneView.setBoundingPolygonsEditingAndConstructing(false); - if(Boolean.TRUE.equals(newValue)) { editorImagePaneView.setDrawingMode(EditorImagePaneView.DrawingMode.BOX); } }); editorToolBarView.getPolygonModeButton().selectedProperty().addListener((observable, oldValue, newValue) -> { - editorImagePaneView.setBoundingPolygonsEditingAndConstructing(false); - if(Boolean.TRUE.equals(newValue)) { editorImagePaneView.setDrawingMode(EditorImagePaneView.DrawingMode.POLYGON); } }); editorToolBarView.getFreehandModeButton().selectedProperty().addListener((observable, oldValue, newValue) -> { - editorImagePaneView.setBoundingPolygonsEditingAndConstructing(false); - if(Boolean.TRUE.equals(newValue)) { editorImagePaneView.setDrawingMode(EditorImagePaneView.DrawingMode.FREEHAND); } diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/controller/SceneKeyShortcutTests.java b/src/test/java/com/github/mfl28/boundingboxeditor/controller/SceneKeyShortcutTests.java index e54bd18..6b7d292 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/controller/SceneKeyShortcutTests.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/controller/SceneKeyShortcutTests.java @@ -21,6 +21,7 @@ import com.github.mfl28.boundingboxeditor.BoundingBoxEditorTestBase; import com.github.mfl28.boundingboxeditor.controller.utils.KeyCombinationEventHandler; import com.github.mfl28.boundingboxeditor.ui.BoundingPolygonView; +import com.github.mfl28.boundingboxeditor.ui.EditorImagePaneView; import javafx.application.Platform; import javafx.event.EventType; import javafx.geometry.Point2D; @@ -113,6 +114,18 @@ void onSceneKeyPressed_ShouldPerformCorrectAction(TestInfo testinfo, FxRobot rob TIMEOUT_DURATION_IN_SEC + " sec.")); + verifyThat(mainView.getEditorImagePane().isDrawingInProgress(), Matchers.equalTo(true)); + verifyThat(mainView.getEditorImagePane().getCurrentBoundingShapeDrawingMode(), + Matchers.equalTo(EditorImagePaneView.DrawingMode.POLYGON)); + + // Clicking outside the imageview should finalize any drawn shapes. + robot.clickOn(mainView.getStatusBar()); + WaitForAsyncUtils.waitForFxEvents(); + + verifyThat(mainView.getEditorImagePane().isDrawingInProgress(), Matchers.equalTo(false)); + verifyThat(mainView.getEditorImagePane().getCurrentBoundingShapeDrawingMode(), + Matchers.equalTo(EditorImagePaneView.DrawingMode.NONE)); + BoundingPolygonView polygon = (BoundingPolygonView) mainView.getCurrentBoundingShapes().get(0); verifyThat(polygon.isSelected(), Matchers.is(true)); verifyThat(polygon, NodeMatchers.isVisible()); @@ -137,6 +150,9 @@ void onSceneKeyPressed_ShouldPerformCorrectAction(TestInfo testinfo, FxRobot rob WaitForAsyncUtils.waitForFxEvents(); + robot.clickOn(mainView.getStatusBar()); + WaitForAsyncUtils.waitForFxEvents(); + Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS, () -> mainView.getCurrentBoundingShapes() .size() == 2), @@ -324,7 +340,6 @@ private void testFocusTagTextFieldKeyEventWhenBoundingShapeSelected(FxRobot robo Platform.runLater(() -> controller.onRegisterSceneKeyReleased(focusTagTextFieldEvent)); WaitForAsyncUtils.waitForFxEvents(); - // No bounding-shapes are selected, therefore tag text-field should be disabled. verifyThat(controller.getView().getTagInputField().isFocused(), Matchers.is(true)); robot.push(KeyCode.ESCAPE); diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java b/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java index bbb667b..045b0ec 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java @@ -419,7 +419,7 @@ void onFreehandDrawing_WhenImageFolderLoaded_ShouldCorrectlyCreatePolygons(FxRob verifyThat(boundingFreehandShapeView.isSelected(), Matchers.equalTo(true), saveScreenshot(testinfo)); verifyThat(mainView.getEditorImagePane().getBoundingShapeSelectionGroup().getSelectedToggle(), Matchers.equalTo(boundingFreehandShapeView), saveScreenshot(testinfo)); - verifyThat(mainView.getEditorImagePane().isFreehandDrawingInProgress(), Matchers.equalTo(true)); + verifyThat(mainView.getEditorImagePane().getCurrentBoundingShapeDrawingMode(), Matchers.equalTo(EditorImagePaneView.DrawingMode.FREEHAND)); int numPathElements = boundingFreehandShapeView.getElements().size(); @@ -453,7 +453,8 @@ void onFreehandDrawing_WhenImageFolderLoaded_ShouldCorrectlyCreatePolygons(FxRob .getSelectedItem().isHasAssignedBoundingShapes(), Matchers.is(true), saveScreenshot(testinfo)); - verifyThat(mainView.getEditorImagePane().isFreehandDrawingInProgress(), Matchers.equalTo(false)); + verifyThat(mainView.getEditorImagePane().getCurrentBoundingShapeDrawingMode(), + Matchers.not(Matchers.equalTo(EditorImagePaneView.DrawingMode.FREEHAND))); verifyThat(mainView.getCurrentBoundingShapes().get(0), Matchers.instanceOf(BoundingPolygonView.class), saveScreenshot(testinfo));