Skip to content

Commit

Permalink
fix(line): Implement better floating point tolerance checks
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey authored and Chris Mackey committed Aug 1, 2023
1 parent e4d3f5e commit fcf2b0f
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 7 deletions.
16 changes: 15 additions & 1 deletion ladybug_geometry/geometry2d/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from .pointvector import Vector2D, Point2D
from ._1d import Base1DIn2D
from ..intersection2d import closest_point2d_between_line2d
from ..intersection2d import closest_point2d_between_line2d, intersect_line2d, \
intersect_line_segment2d


class LineSegment2D(Base1DIn2D):
Expand Down Expand Up @@ -209,6 +210,19 @@ def point_at_length(self, length):
"""
return self.p + self.v * (length / self.length)

def intersect_line_ray(self, line_ray):
"""Get the intersection between this object and another Ray2 or LineSegment2D.
Args:
line_ray: Another LineSegment2D or Ray2D or to intersect.
Returns:
Point2D of intersection if it exists. None if no intersection exists.
"""
if isinstance(line_ray, LineSegment2D):
return intersect_line_segment2d(self, line_ray)
return intersect_line2d(self, line_ray)

def closest_points_between_line(self, line):
"""Get the two closest Point2D between this object to another LineSegment2D.
Expand Down
52 changes: 48 additions & 4 deletions ladybug_geometry/intersection2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,58 @@ def intersect_line2d(line_ray_a, line_ray_b):
if not line_ray_b._u_in(ub):
return None

# final check for co-linearity that escapes floating tolerance
if ua == ub == 0 and math.copysign(1, ua) == -1 and math.copysign(1, ub) == -1:
return None

return Point2D(line_ray_a.p.x + ua * line_ray_a.v.x,
line_ray_a.p.y + ua * line_ray_a.v.y)


def intersect_line_segment2d(line_a, line_b):
"""Get the intersection between two LineSegment2D objects as a Point2D.
This function is identical to intersect_line2d but has some extra checks to
avoid certain cases of floating point tolerance issues. It is only intended
to work with LineSegment2D and not Ray2D.
Args:
line_a: A LineSegment2D object.
line_b: Another LineSegment2D intersect.
Returns:
Point2D of intersection if it exists. None if no intersection exists.
"""
# d is the determinant between lines, if 0 lines are collinear
d = line_b.v.y * line_a.v.x - line_b.v.x * line_a.v.y
if d == 0:
return None

# (dx, dy) = A.p - B.p
dy = line_a.p.y - line_b.p.y
dx = line_a.p.x - line_b.p.x

# Find parameters ua and ub for intersection between two lines

# Calculate scaling parameter for line_b
ua = (line_b.v.x * dy - line_b.v.y * dx) / d
# Checks the bounds of ua to ensure it obeys ray/line behavior
if not line_a._u_in(ua):
return None

# Calculate scaling parameter for line_b
ub = (line_a.v.x * dy - line_a.v.y * dx) / d
# Checks the bounds of ub to ensure it obeys ray/line behavior
if not line_b._u_in(ub):
return None

# compute the intersection point
int_pta = Point2D(line_a.p.x + ua * line_a.v.x, line_a.p.y + ua * line_a.v.y)
int_ptb = Point2D(line_b.p.x + ub * line_b.v.x, line_b.p.y + ub * line_b.v.y)

# if the two points are unequal, there's a floating point tolerance issue
if int_pta != int_ptb:
return None

return int_pta


def intersect_line2d_infinite(line_ray_a, line_ray_b):
"""Get intersection between a Ray2D/LineSegment2D and another extended infinitely.
Expand Down
50 changes: 50 additions & 0 deletions tests/json/colinear_segs_no_int.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
[
{
"v": [
23.513052824460516,
13.575267377674209
],
"p": [
44.636990145612003,
25.771178276387673
],
"type": "LineSegment2D"
},
{
"v": [
40.145947869832412,
23.178273809523944
],
"p": [
0.0,
0.0
],
"type": "LineSegment2D"
}
],
[
{
"v": [
31.162946384563831,
17.991935483872524
],
"p": [
-76.462150503240935,
-36.738839784776701
],
"type": "LineSegment2D"
},
{
"v": [
-13.977914171264274,
-8.0701525094901285
],
"p": [
-25.858542367464672,
-7.5228330056155244
],
"type": "LineSegment2D"
}
]
]
36 changes: 34 additions & 2 deletions tests/line2d_test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# coding=utf-8
import math
import pytest
import json

from ladybug_geometry.geometry2d.pointvector import Point2D, Vector2D
from ladybug_geometry.geometry2d.line import LineSegment2D

import math


def test_linesegment2_init():
"""Test the initialization of LineSegment2D objects and basic properties."""
Expand Down Expand Up @@ -358,6 +358,38 @@ def test_intersect_line_ray_colinear():
assert seg_1.intersect_line_ray(seg_2) == Point2D(0, 0)


def test_intersect_line_ray_colinear_float_tol():
"""Test the LineSegment2D intersect_line_ray method with colinear segments."""
geo_file = './tests/json/colinear_segs_no_int.json'
with open(geo_file, 'r') as fp:
geo_dict = json.load(fp)
segs = [LineSegment2D.from_dict(s) for s in geo_dict[0]]

seg_1, seg_2 = segs
assert seg_1.intersect_line_ray(seg_2) is None
assert seg_2.intersect_line_ray(seg_1) is None
seg_1 = seg_1.flip()
assert seg_1.intersect_line_ray(seg_2) is None
seg_2 = seg_2.flip()
assert seg_1.intersect_line_ray(seg_2) is None


def test_intersect_line_ray_colinear_float_tol2():
"""Test the LineSegment2D intersect_line_ray method with more colinear segments."""
geo_file = './tests/json/colinear_segs_no_int.json'
with open(geo_file, 'r') as fp:
geo_dict = json.load(fp)
segs = [LineSegment2D.from_dict(s) for s in geo_dict[1]]

seg_1, seg_2 = segs
assert seg_1.intersect_line_ray(seg_2) is None
assert seg_2.intersect_line_ray(seg_1) is None
seg_1 = seg_1.flip()
assert seg_1.intersect_line_ray(seg_2) is None
seg_2 = seg_2.flip()
assert seg_1.intersect_line_ray(seg_2) is None


def test_closest_points_between_line():
"""Test the LineSegment2D distance_to_point method."""
pt_1 = Point2D(2, 2)
Expand Down

0 comments on commit fcf2b0f

Please sign in to comment.