Skip to content

Commit fcecfd3

Browse files
committed
Add Line2 tests
* pygorithm/geometry/line2.py - minor documentation changes, add new method are_parallel(line1, line) * tests/test_geometry.py - add Line2 tests
1 parent fd40ece commit fcecfd3

File tree

2 files changed

+214
-15
lines changed

2 files changed

+214
-15
lines changed

pygorithm/geometry/line2.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Line2(object):
1919
points, and thus cannot be changed on their own and must be recalculated
2020
if there were any changes to `start` or `end`.
2121
22-
.. note::
22+
.. tip::
2323
2424
To prevent unnecessary recalculations, many functions on lines accept an
2525
'offset' argument, which is used to perform calculations on lines that
@@ -275,26 +275,38 @@ def calculate_y_intercept(offset):
275275
pass
276276

277277
@staticmethod
278-
def find_intersection(line1, line2, offset1 = None, offset2 = None, find_mtv = True):
278+
def are_parallel(line1, line2):
279+
"""
280+
Determine if the two lines are parallel.
281+
282+
Two lines are parallel if they have the same or opposite slopes.
283+
284+
:param line1: the first line
285+
:type line1: :class:`pygorithm.geometry.line2.Line2`
286+
:param line2: the second line
287+
:type line2: :class:`pygorithm.geometry.line2.Line2`
288+
:returns: if the lines are parallel
289+
:rtype: bool
290+
"""
291+
pass
292+
293+
@staticmethod
294+
def find_intersection(line1, line2, offset1 = None, offset2 = None):
279295
"""
280296
Find the intersection between the two lines.
281297
282298
The lines may optionally be offset by a fixed amount. This
283299
will incur a minor performance penalty which is less than
284300
that of recreating new lines.
285301
286-
.. note::
302+
Two lines are considered touching if they only share exactly
303+
one point and that point is an edge of one of the lines.
287304
288-
There is only a very minor performance improvement by setting find_mtv to
289-
false. It is rare that, if an mtv will be necessary, to find any performance
290-
improvement by first searching with ``find_mtv=False`` and then later
291-
with :``find_mtv=True``
305+
If two lines are parallel, their intersection could be a line.
292306
293-
.. note::
294-
295-
The resulting mtv is broken up into distance and axis. This falls naturally out
296-
of the algorithm and is often convienent. To find the full mtv, simply multiply
297-
the two.
307+
.. tip::
308+
309+
This will never return True, True
298310
299311
:param line1: the first line
300312
:type line1: :class:`pygorithm.geometry.line2.Line2`
@@ -304,9 +316,8 @@ def find_intersection(line1, line2, offset1 = None, offset2 = None, find_mtv = T
304316
:type offset1: :class:`pygorithm.geometry.vector2.Vector2` or None
305317
:param offset2: the offset of line 2
306318
:type offset2: :class:`pygorithm.geometry.vector2.Vector2` or None
307-
:param find_mtv: if false, mtv will always be null.
308-
:returns: (touching, overlapping, (distance, axis))
309-
:rtype: (bool, bool, (:class:`numbers.Number`, :class:`pygorithm.geometry.vector2.Vector2`) or None)
319+
:returns: (touching, overlapping, intersection_location)
320+
:rtype: (bool, bool, :class:`pygorithm.geometry.line2.Line2` or :class:`pygorithm.geometry.vector2.Vector2` or None)
310321
"""
311322
pass
312323

tests/test_geometry.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
import math
3+
import random
34

45
from pygorithm.geometry import (
56
rect_broad_phase,
@@ -213,6 +214,193 @@ def test_magnitude(self):
213214
magn = vec1.magnitude()
214215
self.assertEqual(5, magn)
215216

217+
class TestLine2(unittest.TestCase):
218+
def setUp(self):
219+
random.seed()
220+
221+
self.vec_origin = vector2.Vector2(0, 0)
222+
self.vec_1_1 = vector2.Vector2(1, 1)
223+
self.vec_2_1 = vector2.Vector2(2, 1)
224+
self.vec_1_2 = vector2.Vector2(1, 2)
225+
self.vec_3_4 = vector2.Vector2(3, 4)
226+
self.vec_neg_1_neg_1 = vector2.Vector2(-1, -1)
227+
228+
self.line_origin_1_1 = line2.Line2(self.vec_origin, self.vec_1_1)
229+
self.line_1_1_3_4 = line2.Line2(self.vec_1_1, self.vec_3_4)
230+
self.line_1_1_2_1 = line2.Line2(self.vec_1_1, self.vec_2_1)
231+
self.line_1_1_1_2 = line2.Line2(self.vec_1_1, self.vec_1_2)
232+
233+
def test_constructor(self):
234+
_line = self.line_origin_1_1
235+
236+
self.assertIsNotNone(_line.start)
237+
self.assertIsNotNone(_line.end)
238+
239+
self.assertEqual(0, _line.start.x)
240+
self.assertEqual(0, _line.start.y)
241+
self.assertEqual(1, _line.end.x)
242+
self.assertEqual(1, _line.end.y)
243+
244+
with self.assertRaises(ValueError):
245+
_line2 = line2.Line2(self.vec_origin, self.vec_origin)
246+
247+
def test_delta(self):
248+
self.assertEqual(1, self.line_origin_1_1.delta.x)
249+
self.assertEqual(1, self.line_origin_1_1.delta.y)
250+
self.assertEqual(2, self.line_1_1_3_4.delta.x)
251+
self.assertEqual(3, self.line_1_1_3_4.delta.y)
252+
253+
def test_axis(self):
254+
self.assertAlmostEqual(0.70710678118, self.line_origin_1_1.axis.x)
255+
self.assertAlmostEqual(0.70710678118, self.line_origin_1_1.axis.y)
256+
self.assertAlmostEqual(0.55470019622, self.line_1_1_3_4.axis.x)
257+
self.assertAlmostEqual(0.83205029433, self.line_1_1_3_4.axis.y)
258+
self.assertEqual(1, self.line_1_1_2_1.axis.x)
259+
self.assertEqual(0, self.line_1_1_2_1.axis.y)
260+
self.assertEqual(0, self.line_1_1_1_2.axis.x)
261+
self.assertEqual(1, self.line_1_1_1_2.axis.y)
262+
263+
def test_normal(self):
264+
self.assertAlmostEqual(-0.70710678118, self.line_origin_1_1.normal.x)
265+
self.assertAlmostEqual(0.70710678118, self.line_origin_1_1.normal.y)
266+
self.assertAlmostEqual(-0.83205029433, self.line_1_1_3_4.normal.x)
267+
self.assertAlmostEqual(0.55470019622, self.line_1_1_3_4.normal.y)
268+
self.assertEqual(0, self.line_1_1_2_1.normal.x)
269+
self.assertEqual(1, self.line_1_1_2_1.normal.y)
270+
self.assertEqual(-1, self.line_1_1_1_2.normal.x)
271+
self.assertEqual(0, self.line_1_1_1_2.normal.y)
272+
273+
def test_magnitude_squared(self):
274+
self.assertAlmostEqual(2, self.line_origin_1_1.magnitude_squared)
275+
self.assertAlmostEqual(13, self.line_1_1_3_4.magnitude_squared)
276+
self.assertEqual(1, self.line_1_1_2_1.magnitude_squared)
277+
self.assertEqual(1, self.line_1_1_1_2.magnitude_squared)
278+
279+
def test_magnitude(self):
280+
self.assertAlmostEqual(1.41421356237, self.line_origin_1_1.magnitude)
281+
self.assertAlmostEqual(3.60555127546, self.line_1_1_3_4.magnitude)
282+
self.assertEqual(1, self.line_1_1_2_1.magnitude)
283+
self.assertEqual(1, self.line_1_1_1_2.magnitude)
284+
285+
def test_line_boundaries_x(self): # min_x, min_y, max_x, max_y
286+
_line = line2.Line2(vector2.Vector2(-2, 3), vector2.Vector2(1, -1))
287+
self.assertEqual(-2, _line.min_x)
288+
self.assertEqual(1, _line.max_x)
289+
self.assertEqual(-1, _line.min_y)
290+
self.assertEqual(3, _line.max_y)
291+
292+
def test_slope(self):
293+
self.assertEqual(1, self.line_origin_1_1.slope)
294+
self.assertAlmostEqual(1.5, self.line_1_1_3_4.slope)
295+
self.assertEqual(0, self.line_1_1_1_2.slope)
296+
self.assertEqual(float('+inf'), self.line_1_1_2_1.slope)
297+
298+
def test_y_intercept(self):
299+
self.assertEqual(0, self.line_origin_1_1.y_intercept)
300+
self.assertAlmostEqual(-0.5, self.line_1_1_3_4.y_intercept)
301+
self.assertTrue(math.isnan(self.line_1_1_1_2.y_intercept))
302+
self.assertEqul(1, self.line_1_1_2_1.y_intercept)
303+
304+
def test_horizontal(self):
305+
self.assertFalse(self.line_origin_1_1.horizontal)
306+
self.assertFalse(self.line_1_1_3_4.horizontal)
307+
self.assertFalse(self.line_1_1_1_2.horizontal)
308+
self.assertTrue(self.line_1_1_2_1.horizontal)
309+
310+
def test_vertical(self):
311+
self.assertFalse(self.line_origin_1_1.vertical)
312+
self.assertFalse(self.line_1_1_3_4.vertical)
313+
self.assertTrue(self.line_1_1_1_2.vertical)
314+
self.assertFalse(self.line_1_1_2_1.vertical)
315+
316+
def test_repr(self):
317+
self.assertEqual('line2(start=vector2(x=1, y=1), end=vector2(x=3, y=4))', repr(self.line_1_1_3_4))
318+
319+
def test_str(self):
320+
self.assertEqual('<1, 1> -> <3, 4>', str(self.line_1_1_3_4))
321+
322+
def test_calculate_y_intercept(self):
323+
self.assertAlmostEqual(0.66666666667, self.line_1_1_3_4.calculate_y_intercept(self.vec_1_1))
324+
325+
def test_are_parallel(self):
326+
self.assertFalse(line2.Line2.are_parallel(self.line_origin_1_1, self.line_1_1_3_4))
327+
328+
_line = line2.Line2(vector2.Vector2(5, 4), vector2.Vector2(3, 1))
329+
self.assertTrue(line2.Line2.are_parallel(self.line_1_1_3_4, _line))
330+
331+
@staticmethod
332+
def _find_intr_fuzzer(v1, v2, v3, v4, exp_touching, exp_overlap, exp_intr, number_fuzzes = 3):
333+
for i in range(number_fuzzes):
334+
offset1 = vector2.Vector2(random.randrange(-1000, 1000, 0.01), random.randrange(-1000, 1000, 0.01))
335+
offset2 = vector2.Vector2(random.randrange(-1000, 1000, 0.01), random.randrange(-1000, 1000, 0.01))
336+
337+
_line1 = line2.Line2(v1 - offset1, v2 - offset1)
338+
_line2 = line2.Line2(v3 - offset2, v4 - offset2)
339+
340+
help_msg = 'v1={}, v2={}, offset1={}\n_line1={}\nv3={}, v4={}, offset2={}\n_line2={}'.format(repr(v1), \
341+
repr(v2), repr(offset1), repr(_line1), repr(v3), repr(v4), repr(offset2), repr(_line2))
342+
343+
touching, overlap, intr = line2.Line2.find_intersection(_line1, _line2, offset1, offset2)
344+
self.assertEqual(exp_touching, touching, help_msg)
345+
self.assertEqual(exp_overlap, overlap, help_msg)
346+
347+
if exp_intr is None:
348+
self.assertIsNone(intr, help_msg)
349+
else:
350+
self.assertIsNotNone(intr, help_msg)
351+
352+
if isinstance(exp_intr, vector2.Vector2):
353+
self.assertIsInstance(intr, vector2.Vector2, help_msg)
354+
355+
self.assertAlmostEqual(exp_intr.x, intr.x)
356+
self.assertAlmostEqual(exp_intr.y, intr.y)
357+
else:
358+
self.assertIsInstance(exp_intr, line2.Line2, help_msg)
359+
self.assertIsInstance(intr, line2.Line2, help_msg)
360+
361+
self.assertAlmostEqual(exp_intr.start.x, intr.start.x)
362+
self.assertAlmostEqual(exp_intr.start.y, intr.start.y)
363+
self.assertAlmostEqual(exp_intr.end.x, intr.end.x)
364+
self.assertAlmostEqual(exp_intr.end.y, itnr.end.y)
365+
366+
367+
def test_find_intersection_non_parallel_no_intersection(self):
368+
self._find_intr_fuzzer(vector2.Vector2(3, 4), vector2.Vector2(5, 6),
369+
vector2.Vector2(5, 4), vector2.Vector2(7, 3),
370+
False, False, None)
371+
372+
def test_find_intersection_parallel_no_intersection(self):
373+
self._find_intr_fuzzer(vector2.Vector2(1, 1), vector2.Vector2(3, 3),
374+
vector2.Vector2(2, 1), vector2.Vector2(4, 3),
375+
False, False, None)
376+
377+
def test_find_intersection_non_parallel_intersect_at_edge(self):
378+
self._find_intr_fuzzer(vector2.Vector2(3, 4), vector2.Vector2(5, 6),
379+
vector2.Vector2(1, 6), vector2.Vector2(5, 2),
380+
True, False, vector2.Vector2(3, 4))
381+
382+
def test_find_intersection_non_parallel_intersect_not_edge(self):
383+
self._find_intr_fuzzer(vector2.Vector2(3, 4), vector2.Vector2(5, 6),
384+
vector2.Vector2(3.5, 7), vector2.Vector2(4.5, 4),
385+
False, True, vector2.Vector2(4.125, 5.125))
386+
387+
def test_find_intersection_parallel_intersect_at_edge(self):
388+
self._find_intr_fuzzer(vector2.Vector2(3, 4), vector2.Vector2(5, 6),
389+
vector2.Vector2(5, 6), vector2.Vector2(7, 8),
390+
True, False, vector2.Vector2(5, 6))
391+
392+
def test_find_intersection_parallel_intersect_overlap(self):
393+
self._find_intr_fuzzer(vector2.Vector2(3, 4), vector2.Vector2(5, 6),
394+
vector2.Vector2(4, 5), vector2.Vector2(7, 8),
395+
False, True, line2.Line2(vector2.Vector2(4, 5), vector2.Vector2(5, 6)))
396+
397+
def test_find_intersection_parallel_overlap_compeletely(self):
398+
self._find_intr_fuzzer(vector2.Vector2(3, 4), vector2.Vector2(5, 6),
399+
vector2.Vector2(2, 3), vector2.Vector2(7, 8),
400+
False, True, line2.Line2(vector2.Vector2(3, 4), vector2.Vector2(5, 6)))
401+
402+
403+
216404

217405
if __name__ == '__main__':
218406
unittest.main()

0 commit comments

Comments
 (0)