9 February 2009

Rotated Rectangle Collision – VB.Net

I translated some C code by Oren Becker that determines if two rotated RectangleFs are overlapping. The rectangles are defined by their centre, size and angle of rotation. In the original code the size was actually (Width / 2, Height / 2) which was confusing.

RotatedRectangleF.vb
Public Structure RotatedRectangleF

    Public Const Pi As Single = CType(Math.PI, Single)

    Private m_centre As PointF
    ''' <summary>
    ''' Gets or sets the coordinates of the centre of this RotatedRectangleF structure.
    ''' </summary>    
    ''' <returns>
    ''' A System.Drawing.PointF that represents the centre of this RotatedRectangleF structure.
    ''' </returns>
    Public Property Centre() As PointF
        Get
            Return m_centre
        End Get
        Set(ByVal value As PointF)
            m_centre = value
        End Set
    End Property

    Private m_size As SizeF
    ''' <summary>
    ''' Gets or sets the size of this RotatedRectangleF.
    ''' </summary>    
    ''' <returns>
    ''' A System.Drawing.SizeF that represents the width and height of this RotatedRectangleF structure.
    ''' </returns>
    Public Property Size() As SizeF
        Get
            Return m_size
        End Get
        Set(ByVal value As SizeF)
            m_size = value
        End Set
    End Property

    Private m_angle As Single
    ''' <summary>
    ''' Gets or sets the angle in degrees measured clockwise from the x-axis that this RotatedRectangleF structure is rotated.
    ''' </summary>
    ''' <returns>
    ''' A Single representing the angle in degrees measured clockwise from the x-axis that this RotatedRectangleF structure is rotated.
    ''' </returns>
    Public Property Angle() As Single
        Get
            Return m_angle
        End Get
        Set(ByVal value As Single)
            m_angle = value
        End Set
    End Property

    ''' <summary>
    ''' Initializes a new instance of the RotatedRectangleF stucture with the specified centre, size and angle.
    ''' </summary>
    ''' <param name="centre">The centre of the RotatedRectangleF instance.</param>
    ''' <param name="size">The size of the RotatedRectangleF instance.</param>
    ''' <param name="angle">The angle in degrees clockwise from the x-axis that this RotatedRectangleF is rotated.</param>
    ''' <remarks></remarks>
    Sub New(ByVal centre As PointF, ByVal size As SizeF, ByVal angle As Single)
        Me.Centre = centre
        Me.Size = size
        Me.Angle = angle
    End Sub

    ''' <summary>
    ''' Render this RotatedRectangle using the provided System.Drawing.Graphics object.
    ''' </summary>
    ''' <param name="g">The System.Drawing.Graphics object with which to draw the RotatedRectangleF.</param>    
    Public Sub Render(ByVal g As Graphics, ByVal p As Pen)
        g.TranslateTransform(Me.Centre.X, Me.Centre.Y)
        g.RotateTransform(Me.Angle)
        g.DrawRectangle(p, -Me.Size.Width / 2.0F, -Me.Size.Height / 2.0F, Me.Size.Width, Me.Size.Height)
        g.ResetTransform()
    End Sub

    ''' <summary>
    ''' Determines whether two RotatedRectangleF structures intersect.
    ''' </summary>
    ''' <param name="rr1">The first RotatedRectangleF structure.</param>
    ''' <param name="rr2">The second RotatedRectangleF structure.</param>
    ''' <returns>True if the two RotatedRectangleF structures intersect, False otherwise.</returns>
    ''' <remarks>
    ''' Conversion of code by Oren Becker, 2001
    ''' http://www.ragestorm.net/tutorial?id=22
    ''' </remarks>
    Public Shared Function Intersect(ByVal rr1 As RotatedRectangleF, ByVal rr2 As RotatedRectangleF) As Boolean

        ' Change our structure to match the one in the other code.
        ' Angle in radians, size is (width / 2, height / 2)
        rr1 = New RotatedRectangleF(rr1.Centre, New SizeF(rr1.Size.Width / 2, rr1.Size.Height / 2), DegreesToRadians(rr1.Angle))
        rr2 = New RotatedRectangleF(rr2.Centre, New SizeF(rr2.Size.Width / 2, rr2.Size.Height / 2), DegreesToRadians(rr2.Angle))

        Dim ang As Single = rr1.Angle - rr2.Angle ' orientation of rotated rr1
        Dim cosA As Single = CType(Math.Cos(ang), Single) ' precalculated trigonometic -
        Dim sinA As Single = CType(Math.Sin(ang), Single) ' - values for repeated use

        Dim x, a1 As Single ' temporary variables for various uses 
        Dim dx As Single ' deltaX for linear equations
        Dim ext1, ext2 As Single ' // min/max vertical values

        ' move rr2 to make rr1 cannonic        
        Dim C As New PointF(rr2.Centre.X - rr1.Centre.X, rr2.Centre.Y - rr1.Centre.Y)

        ' rotate rr2 clockwise by rr2->ang to make rr2 axis-aligned
        RotatePointFClockwise(C, rr2.Angle)        

        ' calculate vertices of (moved and axis-aligned := 'ma') rr2
        Dim BL As PointF = C - rr2.Size        
        Dim TR As PointF = C + rr2.Size

        ' calculate vertices of (rotated := 'r') rr1
        Dim A, B As PointF ' vertices of rr2   
        A.X = -rr1.Size.Height * sinA
        B.X = A.X
        Dim temp1 As Single = rr1.Size.Width * cosA
        A.X = A.X + temp1
        B.X = B.X - temp1

        A.Y = rr1.Size.Height * cosA
        B.Y = A.Y
        temp1 = rr1.Size.Width * sinA
        A.Y = A.Y + temp1
        B.Y = B.Y - temp1

        temp1 = sinA * cosA

        ' verify that A is vertical min/max, B is horizontal min/max
        If temp1 < 0 Then
            temp1 = A.X
            A.X = B.X
            B.X = temp1
            temp1 = A.Y
            A.Y = B.Y
            B.Y = temp1
        End If

        ' verify that B is horizontal minimum (leftest-vertex)
        If sinA < 0 Then
            B.X = -B.X
            B.Y = -B.Y
        End If

        ' if rr2(ma) isn't in the horizontal range of
        ' colliding with rr1(r), collision is impossible
        If (B.X > TR.X) OrElse (B.X > -BL.X) Then Return False

        ' if rr1(r) is axis-aligned, vertical min/max are easy to get
        If (temp1 = 0) Then
            ext1 = A.Y
            ext2 = -ext1
        Else
            ' else, find vertical min/max in the range [BL.x, TR.x]
            x = BL.X - A.X
            a1 = TR.X - A.X
            ext1 = A.Y
            ' if the first vertical min/max isn't in (BL.x, TR.x), then
            ' find the vertical min/max on BL.x or on TR.x
            If a1 * x > 0 Then
                dx = A.X
                If x < 0 Then
                    dx -= B.X
                    ext1 -= B.Y
                    x = a1
                Else
                    dx += B.X
                    ext1 += B.Y
                End If
                ext1 *= x
                ext1 /= dx
                ext1 += A.Y
            End If

            x = BL.X + A.X
            a1 = TR.X + A.X
            ext2 = -A.Y
            ' if the second vertical min/max isn't in (BL.x, TR.x), then
            ' find the local vertical min/max on BL.x or on TR.x
            If a1 * x > 0 Then
                dx = -A.X
                If x < 0 Then
                    dx -= B.X
                    ext2 -= B.Y
                    x = a1
                Else
                    dx += B.X
                    ext2 += B.Y
                End If
                ext2 *= x
                ext2 /= dx
                ext2 -= A.Y
            End If
        End If

        ' check whether rr2(ma) is in the vertical range of colliding with rr1(r)
        ' (for the horizontal range of rr2)
        Return Not ((ext1 < BL.Y AndAlso ext2 < BL.Y) OrElse (ext1 > TR.Y AndAlso ext2 > TR.Y))
    End Function

    Private Shared Function DegreesToRadians(ByVal degrees As Single) As Single
        Return degrees * Pi / 180
    End Function

    Private Shared Sub RotatePointFClockwise(ByRef point As PointF, ByVal radians As Single)
        Dim temp As Single = point.X
        Dim cosAngle As Single = CType(Math.Cos(radians), Single)
        Dim sinAngle As Single = CType(Math.Sin(radians), Single)
        point.X = temp * cosAngle + point.Y * sinAngle
        point.Y = -temp * sinAngle + point.Y * cosAngle
    End Sub

    Public Shared Operator =(ByVal rr1 As RotatedRectangleF, ByVal rr2 As RotatedRectangleF) As Boolean
        Return (rr1.Centre = rr2.Centre) AndAlso (rr1.Angle = rr2.Angle) AndAlso (rr1.Size = rr2.Size)
    End Operator

    Public Shared Operator <>(ByVal rr1 As RotatedRectangleF, ByVal rr2 As RotatedRectangleF) As Boolean
        Return Not (rr1 = rr2)
    End Operator

End Structure
And a Form1.vb to demonstrate it:
Imports System.Drawing.Drawing2D

Public Class Form1

    Private selected As Integer = 1
    Private rec1 As New RotatedRectangleF(New PointF(50, -150), New SizeF(100, 100), 10)
    Private rec2 As New RotatedRectangleF(New Point(50, -200), New SizeF(25, 100), 55)
    Private message As String
    Private messageFont As New Font("Courier New", 10, FontStyle.Regular)
    Private selectedRectanglePen As Pen = Pens.Red
    Private unselectedRectanglePen As Pen = Pens.Black

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        message = "Arrow keys / mouse down to move" & vbCrLf
        message &= "Space to select other rectangle." & vbCrLf
        message &= "z x / mouse wheel to rotate."
        Me.DoubleBuffered = True
        Me.ClientSize = New Size(800, 800)
        Me.Text = RotatedRectangleF.Intersect(rec1, rec2).ToString
    End Sub

    Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown
        Select Case e.KeyData
            Case Keys.Left
                MoveSelectedHorizontally(-1)
            Case Keys.Right
                MoveSelectedHorizontally(1)
            Case Keys.Up
                MoveSelectedVertically(-1)
            Case Keys.Down
                MoveSelectedVertically(1)
            Case Keys.Z
                RotateSelected(-1)
            Case Keys.X
                RotateSelected(1)
            Case Keys.Space
                If selected = 1 Then
                    selected = 2
                Else
                    selected = 1
                End If
                Me.Refresh()
        End Select
    End Sub

    Private Sub MouseDidSomething(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseMove, Me.MouseDown
        If e.Button = Windows.Forms.MouseButtons.Left Then
            Dim pos As New PointF(CSng(e.X - Me.ClientSize.Width / 2), CSng(e.Y - Me.ClientSize.Height / 2))
            If selected = 1 Then
                rec1.Centre = pos
            Else
                rec2.Centre = pos
            End If
            Me.Refresh()
            Me.Text = RotatedRectangleF.Intersect(rec1, rec2).ToString
        End If
    End Sub

    Private Sub Form1_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseWheel
        If e.Delta > 0 Then
            RotateSelected(1)
        Else
            RotateSelected(-1)
        End If
    End Sub

    Private Sub RotateSelected(ByVal change As Single)
        If selected = 1 Then
            rec1.Angle += change
        Else
            rec2.Angle += change
        End If
        Me.Refresh()
        Me.Text = RotatedRectangleF.Intersect(rec1, rec2).ToString
    End Sub

    Private Sub MoveSelectedHorizontally(ByVal change As Single)
        If selected = 1 Then
            rec1.Centre = New PointF(rec1.Centre.X + change, rec1.Centre.Y)
        Else
            rec2.Centre = New PointF(rec2.Centre.X + change, rec2.Centre.Y)
        End If
        Me.Refresh()
        Me.Text = RotatedRectangleF.Intersect(rec1, rec2).ToString
    End Sub

    Private Sub MoveSelectedVertically(ByVal change As Single)
        If selected = 1 Then
            rec1.Centre = New PointF(rec1.Centre.X, rec1.Centre.Y + change)
        Else
            rec2.Centre = New PointF(rec2.Centre.X, rec2.Centre.Y + change)
        End If
        Me.Refresh()
        Me.Text = RotatedRectangleF.Intersect(rec1, rec2).ToString
    End Sub

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        e.Graphics.DrawString(message, messageFont, Brushes.Gray, 10, 10)
        e.Graphics.TranslateTransform(Me.ClientSize.Width \ 2, Me.ClientSize.Height \ 2)
        e.Graphics.DrawLine(Pens.Gray, -Me.ClientSize.Width \ 2, 0, Me.ClientSize.Width \ 2, 0)
        e.Graphics.DrawLine(Pens.Gray, 0, -Me.ClientSize.Height \ 2, 0, +Me.ClientSize.Height \ 2)
        Dim store As Matrix = e.Graphics.Transform
        If selected = 1 Then
            rec2.Render(e.Graphics, unselectedRectanglePen)
            e.Graphics.Transform = store
            rec1.Render(e.Graphics, selectedRectanglePen)
        Else
            rec1.Render(e.Graphics, unselectedRectanglePen)
            e.Graphics.Transform = store
            rec2.Render(e.Graphics, selectedRectanglePen)
        End If
    End Sub

    

End Class

1 comment:

  1. Looks good, but I found a case where it produces a false positive. You can verify it yourself with the following values:

    rr1: angle=10, center (151,-196), size (100,100)
    rr2: angle=55, center ( 50, 200), size (25, 100)

    Those two do not overlap yet the routine says they do.

    win32mfc@gmail.com

    // CHRIS

    ReplyDelete