I need to be able to select a number of shapes in my 3d model by drawing a rectangular region, and all shapes that lie in that region are selected.
I can draw the region and select the nodes for if there is only an x or y rotation. But most combinations of x and y give the incorrect result.
I thought it would be a simple matter of getting the mouse and node position in screen coordinates and comparing them, but that isn't working as expected.
In the application below you can draw a region using the right mouse button (you have to click on a sphere to start, i'm not sure why, they mouse events on the subscene are only triggered if you you click on a sphere?). Another right click (again on a sphere) clears the selection.
You can left-click drag to rotate the model (again you have to start on a sphere). After a rotation, of any amount, about the x-axis you can successfully select a region. Likewise a rotation about the y-axis. However a combination of x and y rotation gives the wrong result. For e.g. diagonally drag a node and you get a result as shown below.
Result of selection after x and y rotation
Any thoughts on what is going wrong? or suggestions for other ways to approach this? Thanks in advance
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.Slider;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape3D;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class ScreenSelection extends Application {
private final PerspectiveCamera camera = new PerspectiveCamera(true);
private final Group root = new Group();
private final Group world = new Group();
private final XFormWorld camPiv = new XFormWorld();
private final Slider zoom = new Slider(-100, 0, -50);
private final Button reset = new Button("Reset");
private final Pane pane = new Pane();
private final BorderPane main = new BorderPane();
double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
double mouseFactorX, mouseFactorY;
public void start(Stage stage) throws Exception {
camera.setTranslateZ(zoom.getValue());
reset.setOnAction(eh -> {
camPiv.reset();
zoom.setValue(-50);
});
camera.setFieldOfView(60);
camPiv.getChildren().add(camera);
Collection<Shape3D> world = createWorld();
RectangleSelect rs = new RectangleSelect(main, world);
this.world.getChildren().addAll(world);
root.getChildren().addAll(camPiv, this.world);
SubScene subScene = new SubScene(root, -1, -1, true, SceneAntialiasing.BALANCED);
subScene.setDepthTest(DepthTest.ENABLE);
subScene.setCamera(camera);
subScene.heightProperty().bind(pane.heightProperty());
subScene.widthProperty().bind(pane.widthProperty());
zoom.valueProperty().addListener((o, oldA, newA) -> camera.setTranslateZ(newA.doubleValue()));
HBox controls = new HBox();
controls.getChildren().addAll(new HBox(new Label("Zoom: "), zoom), new HBox(reset));
pane.getChildren().addAll(controls, subScene);
MenuBar menu = new MenuBar(new Menu("File"));
main.setTop(menu);
main.setCenter(pane);
Scene scene = new Scene(main);
subScene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
});
subScene.setOnMouseDragged((MouseEvent me) -> {
if (me.isSecondaryButtonDown()) {
rs.onMouseDragged(me);
} else if (me.isPrimaryButtonDown()) {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
camPiv.ry(mouseDeltaX * 180.0 / subScene.getWidth());
camPiv.rx(-mouseDeltaY * 180.0 / subScene.getHeight());
}
});
subScene.setOnMouseReleased((MouseEvent me) -> {
rs.omMouseDragReleased(me);
});
subScene.setOnMouseClicked((MouseEvent me) -> {
if (me.getButton() == MouseButton.SECONDARY) {
rs.clearSelection();
}
});
stage.setScene(scene);
stage.setWidth(800);
stage.setHeight(800);
stage.show();
}
private Collection<Shape3D> createWorld() {
List<Shape3D> shapes = new ArrayList<Shape3D>();
Random random = new Random(System.currentTimeMillis());
for (int i=0; i<4000; i++) {
double x = (random.nextDouble() - 0.5) * 30;
double y = (random.nextDouble() - 0.5) * 30 ;
double z = (random.nextDouble() - 0.5) * 30 ;
Sphere point = new Sphere(0.2);
point.setMaterial(new PhongMaterial(Color.SKYBLUE));
point.setPickOnBounds(false);
point.getTransforms().add(new Translate(x, y, z));
shapes.add(point);
}
return shapes;
}
public static void main(String[] args) {
launch(args);
}
public class XFormWorld extends Group {
Transform rotation = new Rotate();
Translate translate = new Translate();
public XFormWorld() {
getTransforms().addAll(rotation, translate);
}
public void reset() {
rotation = new Rotate();
getTransforms().set(0, rotation);
}
public void rx(double angle) {
rotation = rotation.createConcatenation(new Rotate(angle, Rotate.X_AXIS));
getTransforms().set(0, rotation);
}
public void ry(double angle) {
rotation = rotation.createConcatenation(new Rotate(angle, Rotate.Y_AXIS));
getTransforms().set(0, rotation);
}
public void tx(double amount) {
translate.setX(translate.getX() + amount);
}
}
public class RectangleSelect {
private static final int START_X = 0;
private static final int START_Y = 1;
private static final int END_X = 2;
private static final int END_Y = 3;
private double[] sceneCoords = new double[2]; //mouse drag x, y in scene coords
private double[] screenCoords = new double[2]; //mouse drag current x, y in screen coords
private double[] boundsInScreenCoords = new double[4]; //top left x, y, bottom right x,y in screen coords
private Collection<Shape3D> world;
private PhongMaterial selected = new PhongMaterial(Color.YELLOW);
private Rectangle rectangle;
public RectangleSelect(Pane pane, Collection<Shape3D> world) {
sceneCoords[START_X] = Double.MIN_VALUE;
sceneCoords[START_Y] = Double.MIN_VALUE;
rectangle = new Rectangle();
rectangle.setStroke(Color.RED);
rectangle.setOpacity(0.0);
rectangle.setMouseTransparent(true);
rectangle.setFill(null);
this.world = world;
pane.getChildren().add(rectangle);
}
public void onMouseDragged(MouseEvent me) {
clearSelection();
if (sceneCoords[START_X] == Double.MIN_VALUE) {
sceneCoords[START_X] = me.getSceneX();
sceneCoords[START_Y] = me.getSceneY();
screenCoords[START_X] = me.getScreenX();
screenCoords[START_Y] = me.getScreenY();
}
double sceneX = me.getSceneX();
double sceneY = me.getSceneY();
double screenX = me.getScreenX();
double screenY = me.getScreenY();
double topX = Math.min(sceneCoords[START_X], sceneX);
double bottomX = Math.max(sceneCoords[START_X], sceneX);
double leftY = Math.min(sceneCoords[START_Y], sceneY);
double rightY = Math.max(sceneCoords[START_Y], sceneY);
boundsInScreenCoords[START_X] = Math.min(screenCoords[START_X], screenX);
boundsInScreenCoords[END_X]= Math.max(screenCoords[START_X], screenX);
boundsInScreenCoords[START_Y] = Math.min(screenCoords[START_Y], screenY);
boundsInScreenCoords[END_Y] = Math.max(screenCoords[START_Y], screenY);
world.forEach(this::selectIfInBounds);
rectangle.setX(topX);
rectangle.setY(leftY);
rectangle.setWidth(bottomX - topX);
rectangle.setHeight(rightY - leftY);
rectangle.setOpacity(1.0);
}
private void selectIfInBounds(Shape3D node) {
Point2D screenCoods = node.localToScreen(0.0, 0.0, 0.0);
if (screenCoods.getX() > boundsInScreenCoords[START_X] &&
screenCoods.getY() > boundsInScreenCoords[START_Y] &&
screenCoods.getX() < boundsInScreenCoords[END_X] &&
screenCoods.getY() < boundsInScreenCoords[END_Y]) {
Material m = node.getMaterial();
node.getProperties().put("material", m);
node.setMaterial(selected);
}
}
private void unselect(Shape3D node) {
Material m = (Material) node.getProperties().get("material");
if (m != null) {
node.setMaterial(m);
}
}
public void omMouseDragReleased(MouseEvent me) {
rectangle.setOpacity(0.0);
sceneCoords[START_X] = Double.MIN_VALUE;
sceneCoords[START_Y] = Double.MIN_VALUE;
}
public void clearSelection() {
world.forEach(this::unselect);
}
}
}