I have a line segment defined by two pointF
s, along with a 2D bounding rectangle. I want to extend the line segment as much as possible in both directions so t
An extended version of the @andredor algorithm to cover all cases (also when the segments are not parallel to the axes - e.g. when the segments are diagonal). With elaborate explanation of the method as documentation.
def extend_line(xmin, ymin, xmax, ymax, x1, y1, x2, y2):
"""
Extend a line so that it reaches the walls of the bbox.
Args:
xmin(int): The very left coordinate of the bbox.
ymin(int): The very top coordinate of the bbox.
xmax(int): The very right coordinate of the bbox.
ymax(int): The very bottom coordinate of the bbox.
x1(int): The start x coordinate of the line.
y1(int): The start y coordinate of the line.
x2(int): The end x coordinate of the line.
y2(int): The end y coordinate of the line.
Returns:
- (list): The start and end (x, y) coordinates of the extended line.
"""
# If we imagine extending the line until it crosses the top wall of the
# bbox at point `(xmin, y_for_xmin)` and then imagine drawing
# perpendicular lines from each point `(x1, y1)`, `(x2, y2)` to the wall
# of the bbox, we end up with 2 perpendicular trianlges with the same
# angles - similar triangles. The rule of the similar triangles is that
# the side lengths of two similar triangles are proportional.
# That's how we get the equal ratios:
# `| y_for_xmin - y1 | / | xmin - x1 | == | y2 - y1 | / | x2 - x1 |`
# After we move some numbers from one to the other side of this equation,
# we get the value for `y_for_xmin`. That's where the line should cross
# the top wall of the bbox. We do the same for all other coordinates.
# NOTE: These calculations are valid if one starts to draw a line from top
# to botton and from left to right. In case the direction is reverted, we
# need to switch the min and max for each point (x, y). We do that below.
y_for_xmin = y1 + (y2 - y1) * (xmin - x1) / (x2 - x1)
y_for_xmax = y1 + (y2 - y1) * (xmax - x1) / (x2 - x1)
x_for_ymin = x1 + (x2 - x1) * (ymin - y1) / (y2 - y1)
x_for_ymax = x1 + (x2 - x1) * (ymax - y1) / (y2 - y1)
# The line is vertical
if (x2 - x1) < (y2 - y1):
# The line is drawn from right to left
if x1 > x2:
# Switch the min and max x coordinates for y,
# because the direction is from right (min) to left (max)
y_for_xmin, y_for_xmax = y_for_xmax, y_for_xmin
# The line is horizontal
else:
# The line is drawn from bottom to top
if y1 > y2:
# Switch the min and max y coordinates for x,
# because the direction is from bottom (min) to top (max)
x_for_ymin, x_for_ymax = x_for_ymax, x_for_ymin
# The line is drawn from right to left
if x1 > x2:
# Get the maximal value for x1.
# When `x_for_ymin < xmin`(line goes out of the
# bbox from the top), we clamp to xmin.
x1 = max(max(int(x_for_ymin), xmin), x1)
# The line is drawn from left to right
else:
# Get the minimal value for x1.
# When `x_for_ymin < xmin`(line goes out of the
# bbox from the top), we clamp to xmin.
x1 = min(max(int(x_for_ymin), xmin), x1)
# Get the maximal value for x2.
# When `x_for_ymax > xmax` (line goes out of the
# bbox from the bottom), we clamp to xmax.
x2 = max(min(int(x_for_ymax), xmax), x2)
# Get the minimal value for y1
# When `y_for_xmin < ymin`(line goes out of the
# bbox from the left), we clamp to ymin.
y1 = min(max(int(y_for_xmin), ymin), ymax)
# Get the minimal value for y2
y2 = min(int(y_for_xmax), ymax)
# Done
return [x1, y1, x2, y2]
Improved andredor's code - Added edge cases for when line intersects top and bottom or left and right edges. The provided code is for Processing to test the algorithm. The first point is set by clicking the mouse and the second point continuously updates with the current mouse pointer position.
int px = 100, py = 100;
void setup() {
size(480, 640);
background(102);
}
void draw() {
stroke(255);
rect(0, 0, 480, 640);
stroke(100);
if (mousePressed == true) {
px = mouseX;
py = mouseY;
}
extendLine(0, 0, 480, 640, px, py, mouseX, mouseY);
}
void extendLine(int xmin, int ymin, int xmax, int ymax, int x1, int y1, int x2, int y2) {
if (y1 == y2) {
line(xmin, y1, xmax, y1);
return;
}
if(x1 == x2) {
line(x1, ymin, x1, ymax);
return;
}
int y_for_xmin = y1 + (y2 - y1) * (xmin - x1) / (x2 - x1);
int y_for_xmax = y1 + (y2 - y1) * (xmax - x1) / (x2 - x1);
int x_for_ymin = x1 + (x2 - x1) * (ymin - y1) / (y2 - y1);
int x_for_ymax = x1 + (x2 - x1) * (ymax - y1) / (y2 - y1);
if (ymin <= y_for_xmin && y_for_xmin <= ymax
&& ymin <= y_for_xmax && y_for_xmax <= ymax) {
line(xmin, y_for_xmin, xmax, y_for_xmax);
return;
} else if (ymin <= y_for_xmin && y_for_xmin <= ymax) {
if (xmin <= x_for_ymax && x_for_ymax <= xmax) {
line(xmin, y_for_xmin, x_for_ymax, ymax);
return;
}
else if(xmin <= x_for_ymin && x_for_ymin <= xmax) {
line(xmin, y_for_xmin, x_for_ymin, ymin);
return;
}
} else if (ymin <= y_for_xmax && y_for_xmax <= ymax){
if (xmin <= x_for_ymin && x_for_ymin <= xmax){
line(x_for_ymin, ymin, xmax, y_for_xmax);
return;
}
if(xmin <= x_for_ymax && x_for_ymax <= xmax){
line(x_for_ymax, ymax, xmax, y_for_xmax);
return;
}
} else if (xmin <= x_for_ymin && x_for_ymin <= xmax
&& xmin <= x_for_ymax && x_for_ymax <= xmax) {
line(x_for_ymin, ymin, x_for_ymax, ymax);
return;
}
}
Define the rectangle as four lines.
Find the intersection between your line and each of the four lines. (How's your highschool geometry?)
Of these four intersection points, determine which points are within the bounds of the rectangle. (find the intersection points where both the x and y values are within the rectangles range).
Your algorithm must also allow for the following edge cases:
Here is an code example in python:
def extend(xmin, ymin, xmax, ymax, x1, y1, x2, y2):
if y1 == y2:
return (xmin, y1, xmax, y1)
if x1 == x2:
return (x1, ymin, x1, ymax)
# based on (y - y1) / (x - x1) == (y2 - y1) / (x2 - x1)
# => (y - y1) * (x2 - x1) == (y2 - y1) * (x - x1)
y_for_xmin = y1 + (y2 - y1) * (xmin - x1) / (x2 - x1)
y_for_xmax = y1 + (y2 - y1) * (xmax - x1) / (x2 - x1)
x_for_ymin = x1 + (x2 - x1) * (ymin - y1) / (y2 - y1)
x_for_ymax = x1 + (x2 - x1) * (ymax - y1) / (y2 - y1)
if ymin <= y_for_xmin <= ymax:
if xmin <= x_for_ymax <= xmax:
return (xmin, y_for_xmin, x_for_ymax, ymax)
if xmin <= x_for_ymin <= xmax:
return (xmin, y_for_xmin, x_for_ymin, ymin)
if ymin <= y_for_xmax <= ymax:
if xmin <= x_for_ymin <= xmax:
return (x_for_ymin, ymin, xmax, y_for_xmax)
if xmin <= x_for_ymax <= xmax:
return (x_for_ymax, ymax, xmax, y_for_xmax)
def test():
assert (2, 1, 2, 5) == extend(1, 1, 5, 5, 2, 3, 2, 4)
assert (1, 2, 4, 5) == extend(1, 1, 5, 5, 2, 3, 3, 4)
assert (1, 3, 5, 3) == extend(1, 1, 5, 5, 3, 3, 4, 3)
assert (1, 1, 5, 5) == extend(1, 1, 5, 5, 2, 2, 3, 3)
assert (3, 1, 5, 5) == extend(1, 1, 5, 5, 3.5, 2, 4, 3)
if __name__ == '__main__':
test()
It doesn't check that the segment is contained in the rectangle and should work also if it is exterior to it (returns None -implicit- if there is no actual segment intersection).
It is based on the assumption that the rectangle has the segments parallel with the axes.
One option would be to define a parametric representation of the line segment ranging over some variable t, then to define four linear equations defining the lines on the side of the box (extended infinitely in all directions). The idea is that when you check where the segment hits these lines, for each direction you could extend the segment, you'll get two intersection points - one for the horizontal intersection and one for the vertical intersection. Whichever of these lies inside the box will be the one that you want to pick.
To do this, compute values of the parameter t of the line formed by extending the segment in each direction where you hit one of the four bounding lines. I assume that the line segment is parameterized such that t ∈ [0, 1]. You will then get (up to) four values of t corresponding to parameters where the line intersects the bounding box - two values ≥ 1 representing extensions of the line in one direction and two values ≤ 0 representing extensions of the line in the other direction. Of these four values, you want to choose the value of t ≥ 1 that's the smallest and the value of t ≥ 0 that's the greatest (these represent the parameters of the line that extend out the shortest distance in each direction before hitting the wall). Once you have these two parameters, plug the values of t back into the original parameterization to yield the two intersection points you want with the walls, then define the new line segment to be one that spans from the first of these to the second.
Note that this algorithm more generally can be used to extend the line segment to fill the bounds of any convex polygon, including rectangles that aren't axis-aligned. You never actually need to test whether any of the points you find are contained in the bounding box; you just look at the value of the parameter t to see which of the intersection points are closer to the endpoints of your segment.
Hope this helps!