Skip to content

Commit 47f6f2a

Browse files
committed
Fix Line3 distance for parallel and non-unit directions
Project principal-point displacement onto the common normal of unit directions, with a scalar perpendicular-distance fallback for parallel lines. Cover intersecting, coincident and skew lines, direction rescaling, operand reversal and the parallel tolerance. AI assistance: prepared with Codex; validation executed locally.
1 parent f6a572c commit 47f6f2a

2 files changed

Lines changed: 41 additions & 15 deletions

File tree

‎spatialmath/geom3d.py‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ def distance(
836836
837837
:param l2: Second line
838838
:type l2: ``Line3``
839-
:param tol: Tolerance in multiples of eps, defaults to 20
839+
:param tol: Parallel-direction tolerance in multiples of eps, defaults to 20
840840
:type tol: float, optional
841841
:return: Closest distance between lines
842842
:rtype: float
@@ -847,20 +847,16 @@ def distance(
847847
848848
:seealso: :meth:`closest_to_line`
849849
"""
850-
if l1 | l2:
851-
# lines are parallel
852-
l = np.cross(
853-
l1.w, l1.v - l2.v * np.dot(l1.w, l2.w) / dot(l2.w, l2.w)
854-
) / np.linalg.norm(l1.w)
855-
else:
856-
# lines are not parallel
857-
if abs(l1 * l2) < tol * _eps:
858-
# lines intersect at a point
859-
l = 0
860-
else:
861-
# lines don't intersect, find closest distance
862-
l = abs(l1 * l2) / np.linalg.norm(np.cross(l1.w, l2.w)) ** 2
863-
return l
850+
w1, w2 = l1.uw, l2.uw
851+
normal = np.cross(w1, w2)
852+
normal_length = np.linalg.norm(normal)
853+
delta = l2.pp - l1.pp
854+
if normal_length <= tol * _eps:
855+
# Parallel lines: remove the component along the common direction.
856+
return float(np.linalg.norm(np.cross(delta, w1)))
857+
# The displacement along the common normal is the shortest distance.
858+
# Unit directions make the result independent of Plucker scaling.
859+
return float(abs(np.dot(delta, normal)) / normal_length)
864860

865861
def closest_to_line(
866862
l1, l2: Line3 # type:ignore

‎tests/test_geom3d.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,36 @@
1717
import matplotlib.pyplot as plt
1818

1919

20+
@pytest.mark.parametrize(
21+
"p1, w1, p2, w2, expected",
22+
[
23+
([0, 0, 0], [1, 0, 0], [0, 3, 4], [1, 0, 0], 5),
24+
([1, 2, 3], [1, 0, 0], [4, 2, 3], [-1, 0, 0], 0),
25+
([1, 2, 3], [2, 0, 0], [1, 2, 3], [0, 5, 0], 0),
26+
([0, 0, 0], [1, 0, 0], [0, 0, 2], [0.6, 0.8, 0], 2),
27+
([1, 2, 3], [2, 0, 0], [1, 2, 5], [3, 4, 0], 2),
28+
],
29+
ids=["parallel", "coincident", "intersecting", "skew-unit", "skew-scaled"],
30+
)
31+
@pytest.mark.parametrize("scale1, scale2", [(1, 1), (2, 5), (-2, 0.25), (1e-8, 1e-8)])
32+
def test_line_distance(p1, w1, p2, w2, expected, scale1, scale2):
33+
# Scaling either set of Plucker coordinates does not change the line.
34+
line1 = Line3.PointDir(p1, np.array(w1) * scale1)
35+
line2 = Line3.PointDir(p2, np.array(w2) * scale2)
36+
for first, second in [(line1, line2), (line2, line1)]:
37+
distance = first.distance(second)
38+
assert np.isscalar(distance)
39+
assert distance == pytest.approx(expected, abs=1e-12)
40+
41+
42+
def test_line_distance_parallel_tolerance():
43+
line1 = Line3.PointDir([0, 0, 0], [1, 0, 0])
44+
line2 = Line3.PointDir([0, 1, 2], [1, 1e-8, 0])
45+
assert line1.distance(line2) == pytest.approx(2)
46+
# A larger angular tolerance treats the directions as parallel.
47+
assert line1.distance(line2, tol=1e8) == pytest.approx(np.sqrt(5))
48+
49+
2050
class Line3Test(unittest.TestCase):
2151
# Primitives
2252
def test_constructor1(self):

0 commit comments

Comments
 (0)