Search code examples
c#graphicsgdi

How to Draw a Rubber Band Selection Rectangle accurately on a Rotated Canvas?


This is a rubber band selection rectangle drawn on a canvas. My problem is that it is easy to get the correct size of the rectangle provided the canvas contents are not rotated. But as soon as it is rotated the rectangle no longer sizes with the cursor. I need the rubber band to stay parallel with screen

   var dragPt = new PointF(e.Position.X - G.ReferenceOffset.X, e.Position.Y - G.ReferenceOffset.Y);

    var rotation = ADEEnvironment.RotateAngle;

    var width = (dragPt.X - pressPt.X);
    var height = (dragPt.Y - pressPt.Y);

The code is pretty trivial. I capture the position of the mouse on mouse down: pressPt. In the mouse move event I get the current mouse position dragPt and calculate the width and height of the rubber band rectangle and use those values to create a rectangle with its origin on pressPt.

This works fine if the camera for the canvas is not rotated. When I rotate the display I need the rubber band to stay aligned with the screen and not the canvas it is drawn on. It I just leave it the rubber band is drawn rotated as well.

If I rotate the rubber band rectangle to return it to alignment with the screen then the rectangle is no longer sizing correctly. So after a lot of messing about I tried a bit of trigonometry:

    var width = (float)((dragPt.X - pressPt.X) / Math.Cos(rotation));
    var height = (float)((dragPt.Y - pressPt.Y) / Math.Cos(rotation));

Which doesn't work and gets very messy given that the rotation angle can be anything for 0 > 360

I have looked at other code on how to create a selection rectangle including the answers to this question: How to make a resizeable rectangle selection tool? but I would like to use the basic code I have if possible since it is related to the graphics engine I am using (Piccolo).

I would put up some screenshots but I can't capture the rubber band. I think this is more of a math problem than anything else and it ought to be easy to fix but I just can't work out what math calculations to make to account to the effect of rotating the display.


Solution

  • This code uses the Paint event to draw

    • One fixed rectangle on a rotated canvas
    • An unrotated copy of it
    • An unrotated rubber-band

    • and checks on the corners of example rectanlge


    // one example 'object'
    Rectangle R0 = new Rectangle(182,82,31,31);
    
    // a few helpers
    Point curMouse = Point.Empty;
    Point downMouse = Point.Empty;
    Rectangle RM = Rectangle.Empty;
    
    float angle = 30;
    Point center = new Point(-55, -22);
    
    private void canvas_Paint(object sender, PaintEventArgs e)
    {
        // preprare the canvas to rotate around a center point:
        e.Graphics.TranslateTransform(center.X , center.Y);
        e.Graphics.RotateTransform(angle);
        e.Graphics.TranslateTransform(-center.X, -center.Y);
        // draw one object and reset
        e.Graphics.DrawRectangle(Pens.Green, R0);
        e.Graphics.ResetTransform();
    
        // for testing (and hittesting): this is the unrotated obejct:
        e.Graphics.DrawRectangle(Pens.LightGray, R0);
    
        // allowing for any way the rubber band is drawn..
        // ..should be moved to a helper function!  
        Size S = new Size( Math.Abs(downMouse.X - curMouse.X), 
                           Math.Abs(downMouse.Y - curMouse.Y));
        Point P0 = new Point(Math.Min(downMouse.X, curMouse.X), 
                           Math.Min(downMouse.Y, curMouse.Y));
        RM = new Rectangle(P0, S);
        // the ruber band
        e.Graphics.DrawRectangle(Pens.Red, RM);
    
    }
    
    private void canvas_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        curMouse = e.Location;
        canvas.Invalidate();
    }
    
    private void canvas_MouseDown(object sender, MouseEventArgs e)
    {
        downMouse = e.Location;
        curMouse = e.Location;
    }
    

    IMO, the more interesting part will be to decide which objects are selected. Will any intersection count or should it be completely contained?

    I found a nice piece of rotation code in this post and add it with an example to check on the fixed Rectangle.

    Of course more complex object will call for more involved lists of points. To get really exact results you may even need to go for GraphicsPaths and the set operations on Regions they support; but maybe a simple convex hull will do..

    Of course, you will want to store the rotated points instead of reapeatedly calculating them..

    static Point RotatePoint(Point pointToRotate, Point centerPoint, double angleInDegrees)
    {
        double angleInRadians = angleInDegrees * (Math.PI / 180);
        double cosTheta = Math.Cos(angleInRadians);
        double sinTheta = Math.Sin(angleInRadians);
        return new Point
        {
            X =
                (int)
                (cosTheta * (pointToRotate.X - centerPoint.X) -
                sinTheta * (pointToRotate.Y - centerPoint.Y) + centerPoint.X),
            Y =
                (int)
                (sinTheta * (pointToRotate.X - centerPoint.X) +
                cosTheta * (pointToRotate.Y - centerPoint.Y) + centerPoint.Y)
        };
    }
    
    private void canvas_MouseUp(object sender, MouseEventArgs e)
    {
    
        List<Point> points = new List<Point>();
        points.Add(RotatePoint(new Point(R0.Left, R0.Top), center, angle));
        points.Add(RotatePoint(new Point(R0.Right, R0.Top), center, angle) );
        points.Add(RotatePoint(new Point(R0.Right, R0.Bottom), center, angle) );
        points.Add(RotatePoint(new Point(R0.Left, R0.Bottom), center, angle));
    
        bool ok = true;
        foreach (Point pt in points) if (!RM.Contains(pt)) ok = false;
    
        if (ok) this.Text = "HIT"; else this.Text = "no hit";
    }