Question

enter image description here

My final goal is to have a method, lets say:

Rectangle snapRects(Rectangle rec1, Rectangle rec2);

Imagine a Rectangle having info on position, size and angle.

Dragging and dropping the ABDE rectangle close to the BCGF rectangle would call the method with ABDE as first argument and BCGF as second argument, and the resulting rectangle is a rectangle lined up with BCGF's edge.

The vertices do not have to match (and preferrably won't so the snapping isn't so restrictive).

I can only understand easily how to give the same angle, but the position change is quite confusing to me. Also, i believe even if i reached a solution it would be quite badly optimized (excessive resource cost), so I would appreciate guidance on this.

(This has already been asked but no satisfatory answer was given and the question forgotten.)

------------------------------------------------------------------

Edit: It seems my explanation was insufficient so I will try to clarify my wishes:

The following image shows the goal of the method in a nutshell:

enter image description here

Forget about "closest rectangle", imagine there are just two rectangles. The lines inside the rectangles represent the direction they are facing (visual aid for the angle).

There is a static rectangle, which is not to be moved and has an angle (0->360), and a rectangle (also with an angle) which I want to Snap to the closest edge of the static rectangle. By this, i mean, i want the least transformations possible for the "snap to edge" to happen.

This brings many possible cases, depending on the rotation of the rectangles and their position relative to each other.

The next image shows the static rectangle and how the position of the "To Snap" rectangle changes the snapping result:

enter image description here

The final rotations might not be perfect since it was done by eye, but you get the point, it matters the relative position and also both angles.

Now, in my point of view, which may be completely naive, I see this problem solved on two important and distinct steps on transforming the "To Snap" rectangle: Positioning and Rotation

Position: The objective of the new position is to stick to the closest edge, but since we want it to stick paralell to the static rectangle, the angle of the static rectangle matters. The next image shows examples of positioning:

enter image description here

In this case, the static rectangle has no angle, so its easy to determine up, down, left and right. But with angle, there are alot more possibilities:

enter image description here

As for the rotation, the goal is for the "to snap" rectangle to rotate the minimum needed to become paralell with the static rectangle:

enter image description here

As a final note, in regard of implementation input, the goal is to actually drag the "to snap" rectangle to wherever position i wish around the static rectangle and by pressing a keyboard key, the snap happens.

Also, it appears i have exagerated a little when i asked for optimization, to be honest i do not need or require optimization, I do prefer an easy to read, step by step clear code (if its the case), rather than any optimization at all.

I hope i was clear this time, sorry for the lack of clarity in the first place, if you have any more doubts please do ask.

Was it helpful?

Solution

The problem is obviously underspecified: What does "line up" for the edges mean? A common start point (but not necessarily a common end point)? A common center point for both edges? (That's what I assumed now). Should ALL edges match? What is the criterion for deciding WHICH edge of the first rectangle should be "matched" with WHICH edge of the second rectangle? That is, imagine one square consists exactly of the center points of the edges of the other square - how should it be aligned then?

(Secondary question: In how far is optimization (or "low resource cost") important?)

However, I wrote a few first lines, maybe this can be used to point out more clearly what the intended behavior should be - namely by saying in how far the intended behavior differs from the actual behavior:

EDIT: Old code omitted, update based on the clarification:

The conditions for the "snapping" are still not unambiguous. For example, it is not clear whether the change in position or the change in the angle should be preferred. But admittedly, I did not figure out in detail all possible cases where this question could arise. In any case, based on the updated question, this might be closer to what you are looking for.

NOTE: This code is neither "clean" nor particularly elegant or efficient. The goal until now was to find a method that delivers "satisfactory" results. Optimizations and beautifications are possible.

The basic idea:

  • Given are the static rectangle r1, and the rectangle to be snapped, r0
  • Compute the edges that should be snapped together. This is divided in two steps:
    • The method computeCandidateEdgeIndices1 computes the "candidate edges" (resp. their indices) of the static rectangle that the moving rectangle may be snapped to. This is based on the folowing criterion: It checks how many vertices (corners) of the moving rectangle are right of the particular edge. For example, if all 4 vertices of the moving rectangle are right of edge 2, then edge 2 will be a candidate for snapping the rectangle to.
    • Since there may be multiple edges for which the same number of vertices may be "right", the method computeBestEdgeIndices computes the candidate edge whose center has the least distance to the center of any edge of the moving rectangle. The indices of the respective edges are returned
  • Given the indices of the edges to be snapped, the angle between these edges is computed. The resulting rectangle will be the original rectangle, rotated by this angle.
  • The rotated rectangle will be moved so that the centers of the snapped edges are at the same point

I tested this with several configurations, and the results at least seem "feasible" for me. Of course, this does not mean that it works satisfactory in all cases, but maybe it can serve as a starting point.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class RectangleSnap
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        RectangleSnapPanel panel = new RectangleSnapPanel();
        f.getContentPane().add(panel);

        f.setSize(1000,1000);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
}

class SnapRectangle
{
    private Point2D position;
    private double sizeX;
    private double sizeY;
    private double angleRad;

    private AffineTransform at;


    SnapRectangle(
        double x, double y, 
        double sizeX, double sizeY, double angleRad)
    {
        this.position = new Point2D.Double(x,y);
        this.sizeX = sizeX;
        this.sizeY = sizeY;
        this.angleRad = angleRad;

        at = AffineTransform.getRotateInstance(
            angleRad, position.getX(), position.getY());
    }

    double getAngleRad()
    {
        return angleRad;
    }

    double getSizeX()
    {
        return sizeX;
    }

    double getSizeY()
    {
        return sizeY;
    }

    Point2D getPosition()
    {
        return position;
    }

    void draw(Graphics2D g)
    {
        Color oldColor = g.getColor();

        Rectangle2D r = new Rectangle2D.Double(
            position.getX(),  position.getY(), sizeX,  sizeY);
        AffineTransform at = AffineTransform.getRotateInstance(
            angleRad, position.getX(), position.getY());
        g.draw(at.createTransformedShape(r));

        g.setColor(Color.RED);
        for (int i=0; i<4; i++)
        {
            Point2D c = getCorner(i);
            Ellipse2D e = new Ellipse2D.Double(c.getX()-3, c.getY()-3, 6, 6);
            g.fill(e);
            g.drawString(""+i, (int)c.getX(), (int)c.getY()+15);
        }

        g.setColor(Color.GREEN);
        for (int i=0; i<4; i++)
        {
            Point2D c = getEdgeCenter(i);
            Ellipse2D e = new Ellipse2D.Double(c.getX()-3, c.getY()-3, 6, 6);
            g.fill(e);
            g.drawString(""+i, (int)c.getX(), (int)c.getY()+15);
        }

        g.setColor(oldColor);
    }

    Point2D getCorner(int i)
    {
        switch (i)
        {
            case 0:
                return new Point2D.Double(position.getX(), position.getY());
            case 1:
            {
                Point2D.Double result = new Point2D.Double(
                    position.getX(), position.getY()+sizeY);
                return at.transform(result, null);
            }
            case 2:
            {
                Point2D.Double result = new Point2D.Double
                    (position.getX()+sizeX, position.getY()+sizeY);
                return at.transform(result, null);
            }
            case 3:
            {
                Point2D.Double result = new Point2D.Double(
                    position.getX()+sizeX, position.getY());
                return at.transform(result, null);
            }
        }
        return null;
    }

    Line2D getEdge(int i)
    {
        Point2D p0 = getCorner(i); 
        Point2D p1 = getCorner((i+1)%4);
        return new Line2D.Double(p0, p1);
    }

    Point2D getEdgeCenter(int i)
    {
        Point2D p0 = getCorner(i); 
        Point2D p1 = getCorner((i+1)%4);
        Point2D c = new Point2D.Double(
            p0.getX() + 0.5 * (p1.getX() - p0.getX()),
            p0.getY() + 0.5 * (p1.getY() - p0.getY()));
        return c;
    }

    void setPosition(double x, double y)
    {
        this.position.setLocation(x, y);
        at = AffineTransform.getRotateInstance(
            angleRad, position.getX(), position.getY());
    }
}


class RectangleSnapPanel extends JPanel implements MouseMotionListener
{
    private final SnapRectangle rectangle0;
    private final SnapRectangle rectangle1;
    private SnapRectangle snappedRectangle0;

    RectangleSnapPanel()
    {
        this.rectangle0 = new SnapRectangle(
            200, 300, 250, 200, Math.toRadians(-21));
        this.rectangle1 = new SnapRectangle(
            500, 300, 200, 150, Math.toRadians(36));
        addMouseMotionListener(this);
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;

        g.setColor(Color.BLACK);
        rectangle0.draw(g);
        rectangle1.draw(g);
        if (snappedRectangle0 != null)
        {
            g.setColor(Color.BLUE);
            snappedRectangle0.draw(g);
        }
    }

    @Override
    public void mouseDragged(MouseEvent e)
    {
        rectangle0.setPosition(e.getX(), e.getY());

        snappedRectangle0 = snapRects(rectangle0, rectangle1);

        repaint();
    }

    @Override
    public void mouseMoved(MouseEvent e)
    {
    }


    private static SnapRectangle snapRects(
        SnapRectangle r0, SnapRectangle r1)
    {
        List<Integer> candidateEdgeIndices1 = 
            computeCandidateEdgeIndices1(r0, r1);

        int bestEdgeIndices[] = computeBestEdgeIndices(
            r0, r1, candidateEdgeIndices1);

        int bestEdgeIndex0 = bestEdgeIndices[0];
        int bestEdgeIndex1 = bestEdgeIndices[1];

        System.out.println("Best to snap "+bestEdgeIndex0+" to "+bestEdgeIndex1);

        Line2D bestEdge0 = r0.getEdge(bestEdgeIndex0);
        Line2D bestEdge1 = r1.getEdge(bestEdgeIndex1);
        double edgeAngle = angleRad(bestEdge0, bestEdge1);
        double rotationAngle = edgeAngle;

        if (rotationAngle <= Math.PI)
        {
            rotationAngle = Math.PI + rotationAngle;
        }
        else if (rotationAngle <= -Math.PI / 2)
        {
            rotationAngle = Math.PI + rotationAngle;
        }
        else if (rotationAngle >= Math.PI)
        {
            rotationAngle = -Math.PI + rotationAngle;
        }

        SnapRectangle result = new SnapRectangle(
            r0.getPosition().getX(), r0.getPosition().getY(), 
            r0.getSizeX(), r0.getSizeY(), r0.getAngleRad()-rotationAngle);

        Point2D edgeCenter0 = result.getEdgeCenter(bestEdgeIndex0);
        Point2D edgeCenter1 = r1.getEdgeCenter(bestEdgeIndex1);
        double dx = edgeCenter1.getX() - edgeCenter0.getX();
        double dy = edgeCenter1.getY() - edgeCenter0.getY();
        result.setPosition(
            r0.getPosition().getX()+dx,
            r0.getPosition().getY()+dy);

        return result;
    }

    // Compute for the edge indices for r1 in the given list
    // the one that has the smallest distance to any edge
    // of r0, and return this pair of indices
    private static int[] computeBestEdgeIndices(
        SnapRectangle r0, SnapRectangle r1,
        List<Integer> candidateEdgeIndices1)
    {
        int bestEdgeIndex0 = -1;
        int bestEdgeIndex1 = -1;
        double minCenterDistance = Double.MAX_VALUE;
        for (int i=0; i<candidateEdgeIndices1.size(); i++)
        {
            int edgeIndex1 = candidateEdgeIndices1.get(i);
            for (int edgeIndex0=0; edgeIndex0<4; edgeIndex0++)
            {
                Point2D p0 = r0.getEdgeCenter(edgeIndex0);
                Point2D p1 = r1.getEdgeCenter(edgeIndex1);
                double distance = p0.distance(p1);
                if (distance < minCenterDistance)
                {
                    minCenterDistance = distance;
                    bestEdgeIndex0 = edgeIndex0;
                    bestEdgeIndex1 = edgeIndex1;
                }
            }
        }
        return new int[]{ bestEdgeIndex0, bestEdgeIndex1 };
    }

    // Compute the angle, in radians, between the given lines,
    // in the range (-2*PI, 2*PI)
    private static double angleRad(Line2D line0, Line2D line1)
    {
        double dx0 = line0.getX2() - line0.getX1();
        double dy0 = line0.getY2() - line0.getY1();
        double dx1 = line1.getX2() - line1.getX1();
        double dy1 = line1.getY2() - line1.getY1();
        double a0 = Math.atan2(dy0, dx0);
        double a1 = Math.atan2(dy1, dx1);
        return (a0 - a1) % (2 * Math.PI);
    }

    // In these methods, "right" refers to screen coordinates, which 
    // unfortunately are upside down in Swing. Mathematically, 
    // these relation is "left"

    // Compute the "candidate" edges of r1 to which r0 may
    // be snapped. These are the edges to which the maximum
    // number of corners of r0 are right of 
    private static List<Integer> computeCandidateEdgeIndices1(
        SnapRectangle r0, SnapRectangle r1)
    {
        List<Integer> bestEdgeIndices = new ArrayList<Integer>();
        int maxRight = 0;
        for (int i=0; i<4; i++)
        {
            Line2D e1 = r1.getEdge(i);
            int right = countRightOf(e1, r0);
            if (right > maxRight)
            {
                maxRight = right;
                bestEdgeIndices.clear();
                bestEdgeIndices.add(i);
            }
            else if (right == maxRight)
            {
                bestEdgeIndices.add(i);
            }
        }
        //System.out.println("Candidate edges "+bestEdgeIndices);
        return bestEdgeIndices;
    }

    // Count the number of corners of the given rectangle
    // that are right of the given line
    private static int countRightOf(Line2D line, SnapRectangle r)
    {
        int count = 0;
        for (int i=0; i<4; i++)
        {
            if (isRightOf(line, r.getCorner(i)))
            {
                count++;
            }
        }
        return count;
    }

    // Returns whether the given point is right of the given line
    // (referring to the actual line *direction* - not in terms 
    // of coordinates in 2D!)
    private static boolean isRightOf(Line2D line, Point2D point)
    {
        double d00 = line.getX1() - point.getX();
        double d01 = line.getY1() - point.getY();
        double d10 = line.getX2() - point.getX();
        double d11 = line.getY2() - point.getY();
        return d00 * d11 - d10 * d01 > 0;
    }

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top