【问题标题】:Snap to Edge Effect对齐边缘效果
【发布时间】:2014-02-08 06:33:49
【问题描述】:

我的最终目标是有一个方法,比如说:

Rectangle snapRects(Rectangle rec1, Rectangle rec2);

想象一个Rectangle 拥有positionsizeangle 的信息。

将 ABDE 矩形拖放到 BCGF 矩形附近会调用以 ABDE 作为第一个参数,BCGF 作为第二个参数的方法,生成的矩形是一个与 BCGF 的边对齐的矩形。

顶点不必匹配(最好不要匹配,因此捕捉没有那么严格)。

我只能很容易理解如何给出相同的角度,但是位置的变化让我很困惑。另外,我相信即使我找到了一个解决方案,它的优化也会很糟糕(资源成本过高),所以我希望能得到这方面的指导。

(这已经被问过了,但没有给出满意的答案,问题被遗忘了。)

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

编辑:看来我的解释不够充分,所以我会尽量澄清我的愿望:

下图简要说明了该方法的目标:

忘掉“最近的矩形”吧,想象一下只有两个矩形。矩形内的线条代表它们所面对的方向(角度的视觉辅助)。

有一个不能移动的静态矩形,它有一个角度(0->360),还有一个我想捕捉到最近边缘的矩形(也有一个角度) > 的静态矩形。我的意思是说,我希望尽可能少的转换以使“对齐边缘”发生。

这会带来许多可能的情况,具体取决于矩形的旋转及其相对于彼此的位置。

下图显示了静态矩形以及“To Snap”矩形的位置如何改变捕捉结果:

最终的旋转可能不是完美的,因为它是通过肉眼完成的,但你明白了,重要的是相对位置和两个角度。

现在,在我看来,这可能是完全幼稚的,我看到这个问题通过两个重要且不同的步骤解决了“To Snap”矩形的变换:定位旋转

位置:新位置的目标是贴在最近的边上,但是由于我们希望它与静态矩形平行,所以静态矩形的角度很重要。下图显示了定位示例:

在这种情况下,静态矩形没有角度,因此很容易确定上下左右。但是有了角度,还有更多的可能性:

至于旋转,目标是让“捕捉”矩形旋转最小,使其与静态矩形平行:

最后一点,关于实现输入,目标是实际将“捕捉”矩形拖动到我希望围绕静态矩形的任何位置,然后通过按下键盘键,捕捉发生。

另外,当我要求优化时,我似乎有点夸大其词,老实说,我不需要或不需要优化,我更喜欢易于阅读、逐步清晰的代码(如果是这样的话),而不是比任何优化都好。

我希望这次我说清楚了,首先抱歉没有说清楚,如果您还有任何疑问,请尽管提问。

【问题讨论】:

  • 当您说“生成的矩形是与 BCGF 的边缘对齐的矩形”时,您到底是什么意思?你的意思是,生成的矩形的边缘应该平行于第二个矩形的边缘吗?如果是这样,ABDE 的旋转方式是否重要,还是应该以尽可能小的角度旋转以使其边缘平行?
  • "我只能很容易理解如何给出相同的角度,但是位置的变化让我很困惑。" - 为什么会让人困惑,你能举个例子吗?另外:您认为当前代码中的哪些内容会耗费“过多的资源”?
  • 捕捉前和捕捉后的图片将是最不言自明的,所以我们实际上看到了你想要实现的目标,我假设第二个矩形是固定的(保持原位)
  • 您是否只是在寻找一种可以旋转/平移 ABDE 以使 BO 垂直于 AC 的算法?
  • 很好的编辑,现在一切都清楚了

标签: java algorithm math methods rectangles


【解决方案1】:

问题显然没有明确说明:边缘的“对齐”是什么意思?一个共同的起点(但不一定是一个共同的终点)?两个边缘的共同中心点? (这就是我现在假设的)。所有边缘都应该匹配吗?决定第一个矩形的哪个边缘应该与第二个矩形的哪个边缘“匹配”的标准是什么?也就是说,想象一个正方形正好由另一个正方形边缘的中心点组成 - 那么它应该如何对齐呢?

(第二个问题:优化(或“低资源成本”)有多重要?)

但是,我写了几行第一行,也许这可以用来更清楚地指出预期的行为应该是什么 - 即说明预期的行为与实际行为的差异有多大:

编辑:省略旧代码,根据说明更新:

“捕捉”的条件仍然不明确。例如,不清楚是应该首选位置变化还是角度变化。但诚然,我并没有详细弄清楚可能出现这个问题的所有可能情况。无论如何,根据更新后的问题,这可能更接近您正在寻找的内容。

注意:这段代码既不“干净”也不特别优雅或高效。迄今为止的目标是找到一种能够提供“令人满意”结果的方法。可以进行优化和美化。

基本思路:

  • 给定静态矩形 r1 和要捕捉的矩形 r0
  • 计算应该对齐的边。这分为两个步骤:
    • computeCandidateEdgeIndices1 方法计算移动矩形可能被捕捉到的静态矩形的“候选边”(分别是它们的索引)。这基于以下标准:它检查移动矩形有多少个顶点(角)是特定边的。例如,如果移动矩形的所有 4 个顶点都是边 2 的 right,则边 2 将成为矩形捕捉到的候选对象。
    • 由于可能存在多个边的“右”顶点数相同,因此computeBestEdgeIndices 方法计算候选边,其中心与移动矩形的任何边的中心的距离最小。返回各个边的索引
  • 给定要捕捉的边的索引,计算这些边之间的角度。生成的矩形将是原始矩形,按此角度旋转。
  • 旋转后的矩形将被移动,以便对齐边缘的中心位于同一点

我用几种配置对此进行了测试,结果至少对我来说似乎“可行”。当然,这并不意味着它在所有情况下都能令人满意,但也许它可以作为一个起点。

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;
    }

}

【讨论】:

  • 我更新了我的问题,很抱歉不够清晰,但是我在 Eclipse 上运行了你的代码,结果仍然与我的预期相差甚远,因为有时捕捉甚至发生在静态矩形的内部,这很奇怪,不会发生 :b 但感谢您到目前为止所做的所有努力,如果它有效,这将是一个很好的例子
  • “奇怪”的案例正是这些尚不清楚应该发生什么的地方。根据更新的答案,我添加了一个 EDIT,也许现在它更接近您正在寻找的内容。
  • 这真的很好,它几乎正是我想要的,不幸的是它仍然有一些像这样的错误:i.imgur.com/5mkE9q2.png?1?4118 但除此之外,这是一个非常好的方法,如果你碰巧完成并清除代码请发布,我也会尝试处理它:)
  • 我明白了,经过几次尝试后,我注意到当两个矩形具有完全相同的角度时会发生这种情况。然后旋转角度计算为 PI 而不是 0,因为查询是针对 (angle>PI) 而不是 (angle>=PI)。现在已修复此问题,但由于浮点数不准确,可能需要引入一些“epsilon 测试”(如 (a >= PI-epsilon))以使其更加“稳健”。
  • 它可能会根据您的实际 Rectangle 类进行清理(当前的“SnapRectangle”只是基于初始描述的快速拍摄)
猜你喜欢
  • 2015-12-19
  • 1970-01-01
  • 2022-01-16
  • 1970-01-01
  • 2012-10-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多