简体   繁体   中英

Determining if a Line intersects with a rotated rectangle on a canvas

I have managed to find code that determines if a Line intersects with a rectangle. My problem is when the Rectangle is rotated . I have looked hi and low to find code that will give me the coordinates of the corners of the rectangle after the transform is performed with no luck. My rectangle when it is rotated is rotated around CenterX and CenterY of 0,0.

I'd appreciate any code that you may have that does this.

Thanks!

More information: I am working on a program that I want to be able to select one or more rectangles on a canvas by drawing a line. The rectangles can be rotated.

I have tried the following code. The second function works properly for non rotated rectangles but not for rotated rectangles.:

public bool AdjustedIntersects(FrameworkElement elem, Line line)
    {
        double x = Canvas.GetLeft(elem);
        double y = Canvas.GetTop(elem);
        double X = x * Math.Cos(((RotateTransform)elem.RenderTransform).Angle)
                - y * Math.Sin(((RotateTransform)elem.RenderTransform).Angle);
        double Y = x * Math.Sin(((RotateTransform)elem.RenderTransform).Angle)
                    + y * Math.Cos(((RotateTransform)elem.RenderTransform).Angle);

        x = Canvas.GetLeft(elem) + elem.Width;
        y = Canvas.GetTop(elem) + elem.Height;

        double X2 = x * Math.Cos(((RotateTransform)elem.RenderTransform).Angle)
                    - y * Math.Sin(((RotateTransform)elem.RenderTransform).Angle);
        double Y2 = x * Math.Sin(((RotateTransform)elem.RenderTransform).Angle)
                    + y * Math.Cos(((RotateTransform)elem.RenderTransform).Angle);

        return SegmentIntersectRectangle(X, Y,X2,Y2,
                                        line.X1, line.Y1, line.X2, line.Y2);

    }

    public bool SegmentIntersectRectangle(
                  double rectangleMinX,
                  double rectangleMinY,
                  double rectangleMaxX,
                  double rectangleMaxY,
                  double p1X,
                  double p1Y,
                  double p2X,
                  double p2Y)
    {
        // Find min and max X for the segment
        double minX = p1X;
        double maxX = p2X;

        if (p1X > p2X)
        {
            minX = p2X;
            maxX = p1X;
        }

        // Find the intersection of the segment's and rectangle's x-projections
        if (maxX > rectangleMaxX)
        {
            maxX = rectangleMaxX;
        }

        if (minX < rectangleMinX)
        {
            minX = rectangleMinX;
        }

        if (minX > maxX) // If their projections do not intersect return false
        {
            return false;
        }

        // Find corresponding min and max Y for min and max X we found before
        double minY = p1Y;
        double maxY = p2Y;

        double dx = p2X - p1X;

        if (Math.Abs(dx) > 0.0000001)
        {
            double a = (p2Y - p1Y) / dx;
            double b = p1Y - a * p1X;
            minY = a * minX + b;
            maxY = a * maxX + b;
        }

        if (minY > maxY)
        {
            double tmp = maxY;
            maxY = minY;
            minY = tmp;
        }

        // Find the intersection of the segment's and rectangle's y-projections
        if (maxY > rectangleMaxY)
        {
            maxY = rectangleMaxY;
        }

        if (minY < rectangleMinY)
        {
            minY = rectangleMinY;
        }

        if (minY > maxY) // If Y-projections do not intersect return false
        {
            return false;
        }

        return true;
    }

A polygon intersects with a line if one of lines of the poligon overlap with the line. Then try this:

  1. Find four conners of the rectangle by the above way.

  2. Check if one of lines make from sequence of conners overlap with the line. I have a ideal for it:

    private bool IsStraightLineOverlap(System.Windows.Shapes.Line line1, Line line2) { var line1Min_X = Math.Min(line1.X1, line1.X2); var line1Max_X = Math.Max(line1.X1, line1.X2); var line1Min_Y = Math.Min(line1.Y1, line1.Y2); var line1Max_Y = Math.Max(line1.Y1, line1.Y2);

     var line2Min_X = Math.Min(line2.X1, line2.X2); var line2Max_X = Math.Max(line2.X1, line2.X2); var line2Min_Y = Math.Min(line2.Y1, line2.Y2); var line2Max_Y = Math.Max(line2.Y1, line2.Y2); var isOverlap_X = (line1Min_X <= line2Max_X && line1Max_X >= line2Min_X); var isOverlap_Y = (line1Min_Y <= line2Max_Y && line1Max_Y >= line2Min_Y); return isOverlap_X && isOverlap_Y; }

In our game suite I have to find rotated points for various purposes.

Here's an extension method:

public static class PointExtension
{
    public static Point GetRotatedPoint(this Point point, Point centerPoint, double angleInDegrees)
    {
        double angleInRadians = angleInDegrees * (Math.PI / 180.0);
        double cosTheta = Math.Cos(angleInRadians);
        double sinTheta = Math.Sin(angleInRadians);
        return new Point
        {
            X =
                (int)
                (cosTheta * (point.X - centerPoint.X) -
                sinTheta * (point.Y - centerPoint.Y) + centerPoint.X),
            Y =
                (int)
                (sinTheta * (point.X - centerPoint.X) +
                cosTheta * (point.Y - centerPoint.Y) + centerPoint.Y)
        };
    }
}

I also have to work out the cells on a grid that a line passes through. To do this I use a bresenham line algorithm. You can google this and find various implementations. Here's mine:

    public static IEnumerable<Point> GetOrderedPointsOnLine(int x0, int y0, int x1, int y1)
    {
        bool steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0);

        if (steep)
        {
            Swap<int>(ref x0, ref y0);
            Swap<int>(ref x1, ref y1);
        }

        int dx = Math.Abs(x1 - x0);
        int dy = Math.Abs(y1 - y0);
        int error = (dx / 2);
        int ystep = (y0 < y1 ? 1 : -1);
        int xstep = (x0 < x1 ? 1 : -1);
        int y = y0;

        for (int x = x0; x != (x1 + xstep); x += xstep)
        {
            yield return new Point((steep ? y : x), (steep ? x : y));
            error = error - dy;
            if (error < 0)
            {
                y += ystep;
                error += dx;
            }
        }
        yield break;
    }

The outline of a rectangle is of course 4 lines.

There is an edge case where both an edge and the line can be a variation of 4 degrees and intersection exactly picks cells don't match. To obviate that you could use a bresenham variation which picks both cells the line partially passes through.

Or you can build two geometries and see if you get anything overlaps when you use the wpf library to detect intersection.

I'm not sure what happens if you apply a transform to a geometry and then use geometry.fillcontainswithdetail

https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.geometry.fillcontainswithdetail?redirectedfrom=MSDN&view=net-5.0#overloads

There's also combinedgeometry to consider. Pick the right options and it'll only give you what overlaps between 2 geometries.

I use neither because in game has to be very fast.

For pre game calculations I render offscreen and examine the bytes of the image I get. This is the fastest simplest way I've found to see which cells (px) an irregular shape occupies. And another way you could approach this.

Solution 1: break the problem down into something that is easier to solve:

  1. Pick any edge on the rectangle, represented by points P1 and P2.
  2. Translate both the points in your rectangle and the points that define your line by -P1, so that the P1 point is now at the origin.
  3. Calculate the angle of your edge ie Math.Atan2(deltaY, deltaX).
  4. Rotate the points for both the rectangle and line in the opposite direction, so that you now have an axis-aligned rectangle.
  5. Do your line/rectangle hit test between these new primitives, using the algorithm you already have for axis-aligned rectangles.
  6. If you need the actual point of intersection in proper world space coordinates then rotate it forward by the angle and translate by +P1.

Solution 2: test the line against each line that forms the rectangle, your problem is now 4 line-to-line intersection tests.

Disclaimer: this is not c#. I only know javascript but the math should be the same.

The way I attack this is by creating an array for my shape that I use to store each vertex. I then use those points to determine where the objects boundaries are. I am assuming that translate and rotate work the same in c# and the objects coordinate are always axis-aligned.

When I draw my rectangle I draw it with the x (left) as -width/2 and y (top) as -height/2 . This is because I am going to use translate to position it where I want it and also allow it to rotate from the center.

    ctx.save();
    ctx.translate(this.x, this.y)
    ctx.rotate(this.rotation);
    ctx.fillStyle = this.color;
    ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height)
    ctx.restore();

I don't know if c# uses save and restore but it would be the same as just translating and rotating it back after ie

    ctx.translate(this.x, this.y);
    ctx.rotate(this.rotation);
    ctx.fillStyle = this.color;
    ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
    ctx.rotate(-this.rotation);
    ctx.translate(-this.x, -this.y)

Also you'll notice in the snippet code that I created an array to store my x and y coordinates of each vertex

this.vertices = [];
    for (let i = 0; i < 4; i++) {
      //initially set to (0, 0) and updated in updateVertices()
      this.vertices.push({ x: 0, y: 0 }); 
    }

This is part of my rectangle object by the array could be global also.

The part that really matters is the math associated to updating the position of the vertices. In this snippet I use a function called updateVertices() . In this function I need to first calculate the sin and cos based on the current rotation.

    let cos = Math.cos(this.rotation); //passing in radians in JS
    let sin = Math.sin(this.rotation); //passing in radians in JS

Once I have that I just update the array of vertices that I created

    //update Top Left Corner
    this.vertices[0].x =
      (this.x - this.centerX) * cos -
      (this.y - this.centerY) * sin +
      (this.centerX - this.width / 2);
    this.vertices[0].y =
      (this.x - this.centerX) * sin +
      (this.y - this.centerY) * cos +
      (this.centerY - this.height / 2);

Do that with all four corners. The math is slightly different for each vertex.

That's it. You have an array with all 4 vertices and can use them how you want. In this example I iterate over them passing two (adjacent) at time to my intersectLines() function to see if my vector lines intersects. Since I am testing 4 edges I use a loop to test all four sides against my vector, I do this in my passToIntersectFunction() function.

In this snippet you can use the mouse to move the vector around and see how the intersect points move.

 let canvas = document.getElementById("canvas"); let ctx = canvas.getContext("2d"); canvas.width = 400; canvas.height = 400; let ptX, ptY; let intersectPoints = []; let mouse = { x: null, y: null }; canvas.addEventListener("mousemove", (e) => { mouse.x = ex - canvas.getBoundingClientRect().x; mouse.y = ey - canvas.getBoundingClientRect().y; }); class Square { constructor() { this.x = 100; this.y = 100; this.width = 50; this.height = 50; this.centerX = this.x + this.width / 2; this.centerY = this.y + this.height / 2; this.color = "red"; this.angle = 0; this.rotation = (this.angle * Math.PI) / 180; //convert to rads //used to store all four vertices this.vertices = []; for (let i = 0; i < 4; i++) { //initially set to (0, 0) and updated in updateVertices() this.vertices.push({ x: 0, y: 0 }); } } draw() { this.angle += 0.5; this.rotation = (this.angle * Math.PI) / 180; ctx.translate(this.x, this.y); ctx.rotate(this.rotation); ctx.fillStyle = this.color; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); ctx.rotate(-this.rotation); ctx.translate(-this.x, -this.y); } drawIntersectPoint() { ctx.fillStyle = "black"; ctx.beginPath(); ctx.arc(ptX, ptY, 3, 0, Math.PI * 2); ctx.fill(); } drawVertices() { ctx.fillStyle = "blue"; ctx.beginPath(); for (let i = 0; i < this.vertices.length; i++) { ctx.arc(this.vertices[i].x, this.vertices[i].y, 3, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); } } updateVertices() { let cos = Math.cos(this.rotation); let sin = Math.sin(this.rotation); //update Top Left Corner this.vertices[0].x = (this.x - this.centerX) * cos - (this.y - this.centerY) * sin + (this.centerX - this.width / 2); this.vertices[0].y = (this.x - this.centerX) * sin + (this.y - this.centerY) * cos + (this.centerY - this.height / 2); //updates Top Right Corner this.vertices[1].x = (this.x + this.width - this.centerX) * cos - (this.y - this.centerY) * sin + (this.centerX - this.width / 2); this.vertices[1].y = (this.x + this.width - this.centerX) * sin + (this.y - this.centerY) * cos + (this.centerY - this.height / 2); //updates Bottom Right Corner this.vertices[2].x = (this.x + this.width - this.centerX) * cos - (this.y + this.height - this.centerY) * sin + (this.centerX - this.width / 2); this.vertices[2].y = (this.x + this.width - this.centerX) * sin + (this.y + this.height - this.centerY) * cos + (this.centerY - this.height / 2); //updates Bottom Left Corner this.vertices[3].x = (this.x - this.centerX) * cos - (this.y + this.height - this.centerY) * sin + (this.centerX - this.width / 2); this.vertices[3].y = (this.x - this.centerX) * sin + (this.y + this.height - this.centerY) * cos + (this.centerY - this.height / 2); } } let square = new Square(); class Vector { constructor() { this.x1 = 200; this.y1 = 100; this.x2 = mouse.x; this.y2 = mouse.y; } draw() { ctx.strokeStyle = "black"; ctx.beginPath(); ctx.moveTo(this.x1, this.y1); ctx.lineTo(this.x2, this.y2); ctx.stroke(); } updateVector() { this.x2 = mouse.x; this.y2 = mouse.y; this.draw(); } } let vector = new Vector(); function intersectLines(coord1, coord2, vector) { //this if statement just keeps the array from constantly growing if (intersectPoints.length > 2) { intersectPoints.shift(); } let x1 = coord1.x; let x2 = coord2.x; let y1 = coord1.y; let y2 = coord2.y; let x3 = vector.x1; let x4 = vector.x2; let y3 = vector.y1; let y4 = vector.y2; let d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (d == 0) { return; } let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / d; let u = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d; if (t > 0 && t < 1 && u > 0) { intersectPoints.push({ x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1) }); } return; } function passToIntersectFunction() { for (let i = 0; i < square.vertices.length; i++) { intersectLines( square.vertices[i], square.vertices[i + 1] ?? square.vertices[0], vector ); } } function drawIntersectPoints() { for (let i = 0; i < intersectPoints.length; i++) { ctx.fillStyle = "black"; ctx.beginPath(); ctx.arc(intersectPoints[i].x, intersectPoints[i].y, 3, 0, Math.PI * 2); ctx.fill(); } } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); square.draw(); square.updateVertices(); square.drawIntersectPoint(); square.drawVertices(); vector.updateVector(); drawIntersectPoints(); passToIntersectFunction(); requestAnimationFrame(animate); } animate();
 <canvas id="canvas"></canvas>

Sorry its not c# but maybe it can help.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM