Search code examples
algorithmlualine

Algorithm to predict if trajectory of a line will come in contact with a rectangle?


I am unsure of what math to use here, as I am very inexperienced in the area of using math along with coding to solve problems such as this, so I was wondering if anyone here could either give me some pointers or give me somewhere to look at this specific problem. I have the x and y trajectory of the single point (or ball) and when it moves, it acts like a line, moving from one place in which is has to stop, then bouncing off of it (reflecting the trajectory), and going in that bounced trajectory. I just need the algorithm to give a true/false (boolean) to whether the current slope will come in contact with the rectangle. I have the 4 edge points of the rectangle and the middle point of the rectangle if needed.


Solution

  • After some back-and-forth in the comments, here is a function that may better suit OP's purpose. The is_intersect() function takes as input the position of the point on the trajectory, its direction, and a quadrilateral, and returns true if the point is on an intersecting trajectory, false otherwise.

    pos is a table containing the position of the point, of the form:

    pos = { x=x1, y=y1 }
    

    dir is a number containing a positive angle in radians (0 <= θ < 2π) with respect to the positive x-axis, representing the direction of travel of the point.

    quad is a table representing a quadrilateral, of the form:

    quad = {{x=x1, y=y1}, {x=x2, y=y2}, {x=x3, y=y3}, {x=x4, y=y4}}
    

    Note that it would be a simple matter, and perhaps desirable, to adapt the code to use integer-indexed tables instead, such as pos = {x1, y1} and quad = {{x1, y1}, {x2, y2},...}.

    This function works for quadrilaterals situated anywhere in the plane, and for trajectory points situated anywhere in the plane. It works by finding the positive angles with respect to the positive x-axis of a line through the trajectory point and each of the corners of the quadrilateral. The function returns true if the direction angle is in this range.

    function is_intersect(pos, dir, quad)
       local theta_corner, theta_min, theta_max
       for i = 1, 4 do
          local x, y = quad[i].x, quad[i].y
    
          -- Find angle of line from pos to corner
          theta_corner = math.atan((y - pos.y) / (x - pos.x))
          -- Adjust angle to quadrant
          if x < pos.x then
             theta_corner = theta_corner + math.pi
          elseif y < pos.y then
             theta_corner = theta_corner + 2*math.pi
          end
    
          -- Keep max and min angles
          if (not theta_min) or theta_corner < theta_min then
             theta_min = theta_corner
          end
          if (not theta_max) or theta_corner > theta_max then
             theta_max = theta_corner
          end
       end
       -- Compare direction angle with max and min angles
       return theta_min <= dir and dir <= theta_max
    end
    

    Here is a sample interaction:

    > test = {{x = 1, y = 1}, {x = 1, y = 2}, {x = 2, y = 2}, {x = 2, y = 1}}
    > position = {x = 3, y = 3}
    > pi = math.pi
    > is_intersect(position, 5*pi/4, test)
    true
    > angle = math.atan(.5)
    > -- checking corners
    > is_intersect(position, pi + angle, test)
    true
    > is_intersect(position, 3*pi/2 - angle, test)
    true
    > -- checking slightly inside corners
    > is_intersect(position, 1.01*(pi + angle), test)
    true
    > is_intersect(position, .99*(3*pi/2 - angle), test)
    true
    > -- checking slightly outside corners
    > is_intersect(position, .99*(pi + angle), test)
    false
    > is_intersect(position, 1.01*(3*pi/2 - angle), test)
    false
    

    How it Works

    This section is for OP's benefit. Read no further if you do not want a Trigonometry refresher.

    enter image description here

    In this diagram, there are two points with directions represented by a green and a yellow arrow. The red lines connect the points with the corners of the rectangle. The is_intersect() function works by calculating the angles, measured from the positive x-axis, as in the diagram, to the lines connecting the point to each of the corners of the rectangle. You can see that there will be four such lines for each point, but only two are marked in the diagram. One of these is the largest such angle, and the other is the smallest. The direction of travel for the point is specified by an angle, also measured from the positive x-axis. If this angle is between the angles to the two red lines, then the point is on an intersecting trajectory with the rectangle. The green point is on an intersecting trajectory: the angle from the positive x-axis to the line of travel for the green point (what we might call its velocity vector) is between the other two angles for this point. But the yellow point is not on an intersecting trajectory: the angle from the positive x-axis to the line of travel for the yellow point is larger than both of the other two angles.

    enter image description here

    Now, in this diagram there are four triangles. Each of the triangles has an angle at the origin of the coordinate system. We define the tangent of this angle to be the ratio of the length of the side opposite the angle (the vertical leg of the triangle) to the length of the side adjacent to the angle (the horizontal leg). That is:

    tan(A) = y/x
    

    Furthermore, the angle A is the arctangent of y/x:

    A = atan(y/x)
    

    enter image description here

    From this diagram, it can be seen that to find the direction angle of the line connecting the point on the trajectory to the corner of the rectangle, we can calculate the angle A from the triangle, and add 270°. We really add 3π/2 radians. For a variety of reasons which I will not go into now, radians are better. Once you get used to them, you will never use degrees for any sort of calculations ever again. If you ever study Calculus, you will have to use radians. But the practical issue at the moment is that Lua's trig functions take arguments in radians, and report angles in radians.

    So, the angle A is atan(x/y). How to find x and y? By subtracting the value of the x-coordinate for the point from the x-coordinate of the rectangle corner we can find x. Similarly, by subtracting the value of the y-coordinate of the point from the y-coordinate of the rectangle corner, we can find y.

    We still need to know which quadrant the angle is in so that we know how much to add to A. The quadrants are traditionally labelled with the Roman numerals from Ⅰ to Ⅳ, starting from the upper right quadrant of the x-y axis. The angle in the diagram is in quadrant Ⅳ. We can tell which quadrant the angle is in by checking to see if the point is to the left or right of the corner of the rectangle, and above or below the corner of the rectangle.

    With this information, the direction angles to each of the corners from the point can be found. The largest and smallest are kept and compared with the direction angle for the trajectory of the point to see if the trajectory will intersect the rectangle.

    The above exposition is a pretty good description of what the code in the is_intersect() function is doing. There is a subtlety in that the subtraction to find the sides of the triangles gives negative side-lengths in some cases. This is a normal issue in trigonometric calculations; the code needs to know how the atan() function being used handles negative values. The code under the comment -- Adjust angle to quadrant takes this into account.

    enter image description here

    For the case of a coordinate system with the origin at the upper left, you simply need to measure angles in a clockwise fashion instead. This is analagous to flipping the original coordinate system upside down. By the way, the original coordinate system (with the origin at the lower-left) is the one usually found in Mathematics, and is called a right-handed coordinate system. The other system, with the origin at the upper-left, is called a left-handed coordinate system.

    Addendum

    In an attempt to help OP understand these functions, I provided this simple testing code. What follows is a further attempt to explicate these tests.

    enter image description here

    The above diagram shows in part the situation represented in the tests. There is a 1X1 rectangle located in quadrant I. Note that these are screen coordinates, with the origin at the upper left. All angles are measured in a clockwise direction from the positive x-axis.

    In the tests you will find:

    pos1 = { x = 0, y = 0 }
    pos3 = { x = 3, y = 3 }
    

    These are two positions from which we wish to test the functions. The first position is at the origin, the second is at the location (3, 3). The red lines in the diagram help us to see the direction angles from the test points to the corners of the rectangle.

    First note that A = atan(1 / 2), or A = atan(.5). This angle will show up again, so I have defined the constants:

    angle = math.atan(.5)
    pi = math.pi
    

    Next notice that the angle B is (π/2 - A) radians. Convince yourself that this is true by looking at the symmetry of the situation. There is no need to convert any of these angles to degrees, in fact do not do this! This will only cause problems; the usual trig functions expect arguments in radians, and return angles in radians.

    Continuing, if you look closely you should be able to see that the angle C is (π + A) radians, and the angle D is (3π/2 - A) radians.

    So we have, for example, these tests:

    { pass = true, args = { pos1, pi/4, rect } },
    { pass = true, args = { pos3, 5*pi/4, rect } },
    

    The first of these tests the trajectory from position pos1, (0, 0), with a direction angle of π/4 radians. This should intersect the corner nearest pos1. The second tests the trajectory from position pos3, (3, 3), with a direction angle of 5π/4 radians. This should intersect the corner nearest pos3. So both tests should return true.

    There are also tests like:

    { pass = true, args = { pos3, pi + angle, rect } },
    { pass = true, args = { pos3, 3*pi/2 - angle, rect } },
    { pass = true, args = { pos3, 1.1*(pi + angle), rect} },
    { pass = false, args = { pos3, 1.1*(3*pi/2 - angle), rect } },
    { pass = false, args = { pos3, .99*(pi + angle), rect } },
    { pass = true, args = { pos3, .99*(3*pi/2 - angle), rect } }, 
    

    Now pi + angle and 3*pi/2 - angle are the direction angles to the outside corners of the rectangle from pos3, as previously noted. So the first two tests should return true, since we expect trajectories directed at the corners to intersect the rectangle. The remaining tests make the direction angles larger or smaller. Looking back at the diagram, note that if the direction angle of the trajectory from pos3 is made larger than C, there should be an intersection, but if it is made smaller than C, there should not be an intersection. Similarly, if the direction angle of the trajectory from pos3 is made larger than D, there should be no intersection, while if it is is made smaller, there should be an intersection.

    All of this is just to explain what is happening in the tests, so that you can understand them and write tests of your own. In practice, you only need to specify the quadrilateral with the quad table, a position with the pos table, and a direction with dir, which is a number in radians. You specified straight-line motion; curvilinear motion is more involved.

    After all of this, I still strongly suggest, as I did earlier in the comments, that you consider using @Gene's function. To use this function you specify a direction vector instead of an angle, using a table: dir = {x = x1, y = y1). This will save you many troubles, and as you mentioned earlier, you already have x- and y-components for the direction of the trajectory. Converting this information to an angle will require you to understand how to make the correct adjustments for the quadrant of the angle. Don't mess with this right now.

    Here are the tests of position pos3 for Gene's function:

    { pass = true, args = { pos3, {x=-1.01, y=-2}, rect} },
    { pass = false, args = { pos3, {x=-0.99, y=-2}, rect } },
    { pass = false, args = { pos3, {x=-2.01, y=-1}, rect } },
    { pass = true, args = { pos3, {x=-1.99, y=-1}, rect } },
    

    Instead of worrying about angles, and referring back to the last diagram, note that arrows from pos3 to the outside corners of the rectangle can be drawn by going 1 in the negative x-direction, and 2 in the negative y-direction, or by going 2 in the negative x-direction and 1 in the negative y-direction. So to test for slightly off-corner intersections, as before, the x-components of the trajectory direction vectors are modified. For example, from pos3, the trajectory {-2, -1} should intersect at the corner nearest the y-axis. So, the trajectory {-2.01, -1} should not intersect with this corner, and the trajectory {-1.99, -1} should intersect.

    One caveat. Notice that there are four tests, and not six as before. Gene's function will report that there is no intersection from pos3 with the trajectory vector {-2, -1}. This is not incorrect, but just a difference in the way his code handles corner cases. In fact, I should probably remove the tests that directly test at the corners of the rectangle for my function, and just test near the corners, which is what really matters. Both functions report the same results for all other cases. The tiniest bit away from a corner, and both functions report false; the tiniest bit inside the corners, and both functions report true.