在窗格上拖动时防止形状重叠
Preventing overlapping shapes while dragging on a Pane
我看过类似的问题,但它们都与碰撞检测有关,而不是防止重叠。我已经将大部分内容用于以下代码:
private final EventHandler<MouseEvent> onPress = mouseEvent -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : getAllShapes()) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
但是,问题是,一旦有一点点重叠,Shape 就根本无法拖动了。意思是,如果我将一个形状拖到另一个形状,一旦它们基本上相切,它们都不再是可拖动的。我想要发生的只是,例如,如果您尝试将一个圆拖到另一个圆上,只要拖动的未来位置会导致重叠,该圆就不会跟随鼠标位置。
我不知道如何完成这个。
编辑:最小可重现示例:
Main.java
public class Main extends Application {
static Circle circle1 = new DraggableCircle(100, 200);
static Circle circle2 = new DraggableCircle(200, 300);
static Circle[] circleList = new Circle[]{circle1, circle2};
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World");
Pane pane = new Pane();
primaryStage.setScene(new Scene(pane, 300, 275));
primaryStage.show();
pane.getChildren().addAll(circle1, circle2);
}
public static void main(String[] args) {
launch(args);
}
}
DraggableCircle.java
public class DraggableCircle extends Circle {
private double xDrag, yDrag;
public DraggableCircle(double x, double y) {
super(x, y, 30);
this.setFill(Color.WHITE);
this.setStroke(Color.BLACK);
this.setStrokeWidth(1.5);
this.addEventHandler(MouseEvent.MOUSE_PRESSED, onPress);
this.addEventHandler(MouseEvent.MOUSE_DRAGGED, onDrag);
}
private final EventHandler<MouseEvent> onPress = (mouseEvent) -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
}
这还有一个问题,即在拖动检测结束之前,拖动过快会导致圆圈之间出现明显的重叠。
一个简单(不完美)的解决方案
以下算法将允许节点在发生交叉后继续被拖动:
- 记录当前可拖动形状的位置。
- 设置新位置。
- 检查路口。
- 如果检测到交叉点,将位置重置为原始位置。
一个实现替换了问题中提供的最小示例代码中的拖动处理程序。
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
this.setCenterX(priorCenterX);
this.setCenterY(priorCenterY);
return;
}
}
}
};
这个处理程序确实比你的处理程序工作得更好,它至少允许你在相交后继续拖动。
但是,是的,如果您快速拖动,它会在检测到拖动操作会导致相交时在节点之间留下可见的 space,这并不理想。
此外,您在评论中添加的关于让拖动的形状沿边界滑动的额外要求需要更复杂的解决方案。
其他可能的解决方案
我在这里不提供这些更复杂的解决方案的代码。
一个潜在的暴力解决方案是用新的中心插入先前的中心,然后在循环中,沿着插入的线缓慢移动拖动的对象,直到检测到交叉点,然后将其退回到最后一个插值以防止相交。您可以通过计算和应用归一化(1 个单位距离)运动向量来实现。这可能会修复相交节点之间的 space。
与获得滑行类似,在交叉点上,您可以只更新内插的 x 或 y 值,而不是同时更新两者。
应用几何数学可能有更复杂的方法,尤其是当您了解形状几何以及运动矢量和表面法线时。
+1 @jewelsea 回答。
除了@jewelsea 的回答之外,我想为“节点之间的space”问题提供修复。
所以您可能已经观察到,当您快速拖动时,它不会覆盖拖动路径中的每个像素。它随着拖动的速度而变化。所以当你决定把它移动到之前记录的点时,我们会做一个快速的数学运算,看看两个节点之间是否有空隙,如果有:
我们将进行数学运算“确定距离为 d 的直线上的一个点”,然后将拖动圆圈移动到该点。这里..
- 线的起点是:上一个记录点
- 线的终点是:相交的形状中心
- d 是:两个形状之间的间隙。
所以@jewelsea 答案的更新代码如下:
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Circle shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
Point2D cx = new Point2D(priorCenterX, priorCenterY);
Point2D px = new Point2D(shape.getCenterX(), shape.getCenterY());
double d = cx.distance(px);
if (d > getRadius() + shape.getRadius()) {
cx = pointAtDistance(cx, px, d - getRadius() - shape.getRadius());
}
this.setCenterX(cx.getX());
this.setCenterY(cx.getY());
return;
}
}
}
};
private Point2D pointAtDistance(Point2D p1, Point2D p2, double distance) {
double lineLength = p1.distance(p2);
double t = distance / lineLength;
double dx = ((1 - t) * p1.getX()) + (t * p2.getX());
double dy = ((1 - t) * p1.getY()) + (t * p2.getY());
return new Point2D(dx, dy);
}
我看过类似的问题,但它们都与碰撞检测有关,而不是防止重叠。我已经将大部分内容用于以下代码:
private final EventHandler<MouseEvent> onPress = mouseEvent -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : getAllShapes()) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
但是,问题是,一旦有一点点重叠,Shape 就根本无法拖动了。意思是,如果我将一个形状拖到另一个形状,一旦它们基本上相切,它们都不再是可拖动的。我想要发生的只是,例如,如果您尝试将一个圆拖到另一个圆上,只要拖动的未来位置会导致重叠,该圆就不会跟随鼠标位置。
我不知道如何完成这个。
编辑:最小可重现示例:
Main.java
public class Main extends Application {
static Circle circle1 = new DraggableCircle(100, 200);
static Circle circle2 = new DraggableCircle(200, 300);
static Circle[] circleList = new Circle[]{circle1, circle2};
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World");
Pane pane = new Pane();
primaryStage.setScene(new Scene(pane, 300, 275));
primaryStage.show();
pane.getChildren().addAll(circle1, circle2);
}
public static void main(String[] args) {
launch(args);
}
}
DraggableCircle.java
public class DraggableCircle extends Circle {
private double xDrag, yDrag;
public DraggableCircle(double x, double y) {
super(x, y, 30);
this.setFill(Color.WHITE);
this.setStroke(Color.BLACK);
this.setStrokeWidth(1.5);
this.addEventHandler(MouseEvent.MOUSE_PRESSED, onPress);
this.addEventHandler(MouseEvent.MOUSE_DRAGGED, onDrag);
}
private final EventHandler<MouseEvent> onPress = (mouseEvent) -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
}
这还有一个问题,即在拖动检测结束之前,拖动过快会导致圆圈之间出现明显的重叠。
一个简单(不完美)的解决方案
以下算法将允许节点在发生交叉后继续被拖动:
- 记录当前可拖动形状的位置。
- 设置新位置。
- 检查路口。
- 如果检测到交叉点,将位置重置为原始位置。
一个实现替换了问题中提供的最小示例代码中的拖动处理程序。
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
this.setCenterX(priorCenterX);
this.setCenterY(priorCenterY);
return;
}
}
}
};
这个处理程序确实比你的处理程序工作得更好,它至少允许你在相交后继续拖动。
但是,是的,如果您快速拖动,它会在检测到拖动操作会导致相交时在节点之间留下可见的 space,这并不理想。
此外,您在评论中添加的关于让拖动的形状沿边界滑动的额外要求需要更复杂的解决方案。
其他可能的解决方案
我在这里不提供这些更复杂的解决方案的代码。
一个潜在的暴力解决方案是用新的中心插入先前的中心,然后在循环中,沿着插入的线缓慢移动拖动的对象,直到检测到交叉点,然后将其退回到最后一个插值以防止相交。您可以通过计算和应用归一化(1 个单位距离)运动向量来实现。这可能会修复相交节点之间的 space。
与获得滑行类似,在交叉点上,您可以只更新内插的 x 或 y 值,而不是同时更新两者。
应用几何数学可能有更复杂的方法,尤其是当您了解形状几何以及运动矢量和表面法线时。
+1 @jewelsea 回答。 除了@jewelsea 的回答之外,我想为“节点之间的space”问题提供修复。
所以您可能已经观察到,当您快速拖动时,它不会覆盖拖动路径中的每个像素。它随着拖动的速度而变化。所以当你决定把它移动到之前记录的点时,我们会做一个快速的数学运算,看看两个节点之间是否有空隙,如果有:
我们将进行数学运算“确定距离为 d 的直线上的一个点”,然后将拖动圆圈移动到该点。这里..
- 线的起点是:上一个记录点
- 线的终点是:相交的形状中心
- d 是:两个形状之间的间隙。
所以@jewelsea 答案的更新代码如下:
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Circle shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
Point2D cx = new Point2D(priorCenterX, priorCenterY);
Point2D px = new Point2D(shape.getCenterX(), shape.getCenterY());
double d = cx.distance(px);
if (d > getRadius() + shape.getRadius()) {
cx = pointAtDistance(cx, px, d - getRadius() - shape.getRadius());
}
this.setCenterX(cx.getX());
this.setCenterY(cx.getY());
return;
}
}
}
};
private Point2D pointAtDistance(Point2D p1, Point2D p2, double distance) {
double lineLength = p1.distance(p2);
double t = distance / lineLength;
double dx = ((1 - t) * p1.getX()) + (t * p2.getX());
double dy = ((1 - t) * p1.getY()) + (t * p2.getY());
return new Point2D(dx, dy);
}