Skip to content

Commit f0613de

Browse files
committed
Part of Polygon tests
First chunk of polygon tests * pygorithm/geometry/polygon2.py - normals cannot be a set (vector2 is not hashable). Improve documentation and add additional options for polygon2 from_regular. Add from_rotated and project_onto_axis * tests/test_geometry.py - add test polygon constructor, from_regular, from_rotated, area, and project_onto_axis. Add skeleton functions for remainder
1 parent 3618040 commit f0613de

File tree

2 files changed

+284
-6
lines changed

2 files changed

+284
-6
lines changed

pygorithm/geometry/polygon2.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,23 @@ class Polygon2(object):
3535
unless necessary through the use of Axis-Aligned Bounding Boxes
3636
and similar tools.
3737
38-
:ivar points: the ordered set of points on this polygon
38+
.. caution::
39+
40+
The normals in the :py:attribute:~pygorithm.geometry.polygon2.Polygon2.normals:
41+
are not necessarily the same length as
42+
:py:attribute:~pygorithm.geometry.polygon2.Polygon2.points: or
43+
:py:attribute:~pygorithm.geometry.polygon2.Polygon2.lines:. It is only
44+
guarranteed to have no two vectors that are the same or opposite
45+
directions, and contain either the vector in the same direction or opposite
46+
direction of the normal vector for every line in the polygon.
47+
48+
:ivar points: the ordered list of points on this polygon
3949
:vartype points: list of :class:`pygorithm.geometry.vector2.Vector2`
4050
41-
:ivar lines: the ordered set of lines on this polygon
51+
:ivar lines: the ordered list of lines on this polygon
4252
:vartype lines: list of :class:`pygorithm.geometry.line2.Line2`
4353
44-
:ivar normals: the ordered set of normals on this polygon
54+
:ivar normals: the unordered list of unique normals on this polygon
4555
:vartype normals: list of :class:`pygorithm.geometry.vector2.Vector2`
4656
4757
:ivar center: the center of this polygon when unshifted.
@@ -76,19 +86,93 @@ def __init__(self, points, suppress_errors = False):
7686
pass
7787

7888
@classmethod
79-
def from_regular(cls, sides, length):
89+
def from_regular(cls, sides, length, start_rads = None, start_degs = None, center = None):
8090
"""
8191
Create a new regular polygon.
8292
93+
.. hint::
94+
95+
If no rotation is specified there is always a point at ``(length, 0)``
96+
97+
If no center is specified, the center will be ``(length / 2, length / 2)``
98+
which makes the top-left bounding box of the polygon the origin.
99+
100+
May specify the angle of the first point. For example, if the coordinate
101+
system is x to the right and y upward, then if the starting offset is 0
102+
then the first point will be at the right and the next point counter-clockwise.
103+
104+
This would make for the regular quad (sides=4) to look like a diamond. To make
105+
the bottom side a square, the whole polygon needs to be rotated 45 degrees, like
106+
so:
107+
108+
.. code-block:: python
109+
110+
from pygorithm.geometry import (vector2, polygon2)
111+
import math
112+
113+
# This is a diamond shape (rotated square) (0 degree rotation assumed)
114+
diamond = polygon2.Polygon2.from_regular(4, 1)
115+
116+
# This is a flat square
117+
square = polygon2.Polygon2.from_regular(4, 1, start_degs = 45)
118+
119+
# Creating a flat square with radians
120+
square2 = polygon2.Polygon2.from_regular(4, 1, math.pi / 4)
121+
83122
:param sides: the number of sides in the polygon
84123
:type sides: :class:`numbers.Number`
85124
:param length: the length of each sides
86125
:type length: :class:`numbers.Number`
126+
:param start_rads: the starting radians or None
127+
:type start_rads: :class:`numbers.Number` or None
128+
:param start_degs: the starting degrees or None
129+
:type start_degs: :class:`numbers.Number` or None
130+
:param center: the center of the polygon
131+
:type center: :class:`pygorithm.geometry.vector2.Vector2`
132+
:returns: the new regular polygon
133+
:rtype: :class:`pygorithm.geometry.polygon2.Polygon2`
87134
88135
:raises ValueError: if ``sides < 3`` or ``length <= 0``
136+
:raises ValueError: if ``start_rads is not None and start_degs is not None``
89137
"""
90138
pass
91139

140+
@classmethod
141+
def from_rotated(cls, original, rotation, rotation_degrees = None):
142+
"""
143+
Create a regular polygon that is a rotation of
144+
a different polygon.
145+
146+
The rotation must be in radians, or null and rotation_degrees
147+
must be specified. Positive rotations are clockwise.
148+
149+
Examples:
150+
151+
.. code-block:: python
152+
153+
from pygorithm.goemetry import (vector2, polygon2)
154+
import math
155+
156+
poly = polygon2.Polygon2.from_regular(4, 1)
157+
158+
# the following are equivalent (within rounding)
159+
rotated1 = polygon2.Polygon2.from_rotated(poly, math.pi / 4)
160+
rotated2 = polygon2.Polygon2.from_rotated(poly, None, 45)
161+
162+
:param original: the polygon to rotate
163+
:type original: :class:`pygorithm.geometry.polygon2.Polygon2`
164+
:param rotation: the rotation in radians or None
165+
:type rotation: :class:`numbers.Number`
166+
:param rotation_degrees: the rotation in degrees or None
167+
:type rotation_degrees: :class:`numbers.Number`
168+
:returns: the rotated polygon
169+
:rtype: :class:`pygorithm.geometry.polygon2.Polygon2`
170+
171+
:raises ValueError: if ``rotation is not None and rotation_degrees is not None``
172+
:raises ValueError: if ``rotation is None and rotation_degrees is None``
173+
"""
174+
pass
175+
92176
@property
93177
def area(self):
94178
"""
@@ -99,6 +183,23 @@ def area(self):
99183
"""
100184
pass
101185

186+
187+
@staticmethod
188+
def project_onto_axis(polygon, offset, axis):
189+
"""
190+
Find the projection of the polygon along the axis.
191+
192+
:param polygon: the polygon to project
193+
:type polygon: :class:`pygorithm.geometry.polygon2.Polygon2`
194+
:param offset: the offset of the polygon
195+
:type offset: :class:`pygorithm.geometry.vector2.Vector2`
196+
:param axis: the axis to project onto
197+
:type axis: :class:`pygorithm.geometry.vector2.Vector2`
198+
:returns: the projection of the polygon along the axis
199+
:rtype: :class:`pygorithm.geometry.axisall.AxisAlignedLine`
200+
"""
201+
pass
202+
102203
@staticmethod
103204
def contains_point(polygon, offset, point):
104205
"""

tests/test_geometry.py

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,11 +488,11 @@ def test_find_intersection_overlapping(self):
488488

489489
touching, mtv = axisall.AxisAlignedLine.find_intersection(_aal1, _aal2)
490490
self.assertFalse(touching)
491-
self.assertEquals(-1, mtv)
491+
self.assertEqual(-1, mtv)
492492

493493
touching, mtv = axisall.AxisAlignedLine.find_intersection(_aal2, _aal1)
494494
self.assertFalse(touching)
495-
self.assertEquals(1, mtv)
495+
self.assertEqual(1, mtv)
496496

497497
def test_contains_point_false(self):
498498
_aal1 = axisall.AxisAlignedLine(self.vec_1_1, 0, 1)
@@ -526,7 +526,184 @@ def test_contains_point_inner(self):
526526
outer, inner = axisall.AxisAlignedLine.contains_point(_aal1, 0.75)
527527
self.assertFalse(outer)
528528
self.assertTrue(inner)
529+
530+
class TestPolygon(unittest.TestCase):
531+
def setUp(self):
532+
random.seed()
533+
534+
def test_constructor_standard(self):
535+
poly = polygon2.Polygon2([ vector2.Vector2(0, 1),
536+
vector2.Vector2(1, 1),
537+
vector2.Vector2(1, 0),
538+
vectro2.Vector2(0, 0) ])
539+
540+
self.assertEqual(4, len(poly.points))
541+
self.assertEqual(4, len(poly.lines))
542+
self.assertEqual(2, len(poly.normals))
543+
544+
self.assertEqual(0, poly.points[0].x)
545+
self.assertEqual(1, poly.points[0].y)
546+
self.assertEqual(1, poly.points[1].x)
547+
self.assertEqual(1, poly.points[1].y)
548+
self.assertEqual(1, poly.points[2].x)
549+
self.assertEqual(0, poly.points[2].y)
550+
self.assertEqual(0, poly.points[3].x)
551+
self.assertEqual(0, poly.points[3].y)
552+
553+
self.assertEqual(0, poly.lines[0].start.x)
554+
self.assertEqual(1, poly.lines[0].start.y)
555+
self.assertEqual(1, poly.lines[0].end.x)
556+
self.assertEqual(1, poly.lines[0].end.y)
557+
self.assertEqual(1, poly.lines[1].start.x)
558+
self.assertEqual(1, poly.lines[1].start.y)
559+
self.assertEqual(1, poly.lines[1].end.x)
560+
self.assertEqual(0, poly.lines[1].end.y)
561+
self.assertEqual(1, poly.lines[2].start.x)
562+
self.assertEqual(0, poly.lines[2].start.y)
563+
self.assertEqual(0, poly.lines[2].end.x)
564+
self.assertEqual(0, poly.lines[2].end.y)
565+
self.assertEqual(0, poly.lines[3].start.x)
566+
self.assertEqual(0, poly.lines[3].start.y)
567+
self.assertEqual(0, poly.lines[3].end.x)
568+
self.assertEqual(1, poly.lines[3].end.y)
569+
570+
self.assertIsNotNone(next(vec for vec in poly.normals if vec.horizontal, None))
571+
self.assertIsNotNone(next(vec for vec in poly.normals if vec.vertical, None))
572+
573+
self.assertAlmostEqual(0.5, poly.center.x)
574+
self.assertAlmostEqual(0.5, poly.center.y)
575+
576+
poly2 = polygon2.Polygon2([ (0, 1), (1, 1), (1, 0), (0, 0) ])
577+
578+
self.assertEqual(4, len(poly2.points))
579+
self.assertEqual(4, len(poly2.lines))
580+
self.assertEqual(2, len(poly2.normals))
581+
582+
with self.assertRaises(StopIteration):
583+
next(i for i in range(4) if poly.points[i].x != poly2.points[i].x or poly.points[i].y != poly2.points[i].y)
584+
585+
586+
def test_from_regular(self):
587+
diamond = polygon2.Polygon2.from_regular(4, 1)
588+
589+
self.assertEqual(2, diamond.points[0].x)
590+
self.assertEqual(1, diamond.points[0].y)
591+
self.assertEqual(1, diamond.points[1].x)
592+
self.assertEqual(0, diamond.points[1].y)
593+
self.assertEqual(0, diamond.points[2].x)
594+
self.assertEqual(1, diamond.points[2].y)
595+
self.assertEqual(1, diamond.points[3].x)
596+
self.assertEqual(2, diamond.points[3].y)
597+
598+
diamond_shifted = polygon2.Polygon2.from_regular(4, 1, center = vector2.Vector2(0, 0))
599+
600+
with self.assertRaises(StopIteration):
601+
next(i for i in range(4) if diamond.points[i].x != diamond_shifted.points[i].x + 1 or diamond.points[i].y != diamond_shifted.points[i].y + 1)
602+
603+
square = polygon2.Polygon2.from_regular(4, 1, math.pi / 4)
604+
605+
self.assertEqual(0, poly.points[0].x)
606+
self.assertEqual(1, poly.points[0].y)
607+
self.assertEqual(1, poly.points[1].x)
608+
self.assertEqual(1, poly.points[1].y)
609+
self.assertEqual(1, poly.points[2].x)
610+
self.assertEqual(0, poly.points[2].y)
611+
self.assertEqual(0, poly.points[3].x)
612+
self.assertEqual(0, poly.points[3].y)
613+
614+
square2 = polygon2.Polygon2.from_regular(4, 1, start_degs = 45)
615+
616+
with self.assertRaises(StopIteration):
617+
next(i for i in range(4) if square.points[i].x != square2.points[i].x or square.points[i].y != square2.points[i].y)
618+
619+
620+
def test_from_rotated(self):
621+
# isos triangle
622+
# weighted total = (0 + 1 + 2, 0 + 1 + 1) = (3, 2)
623+
# center = (1, 2/3)
624+
triangle = polygon2.Polygon2([ (0, 0), (1, 1), (2, 1) ])
625+
626+
triangle_rot = polygon2.Polygon2.from_rotated(triangle, math.pi / 4)
627+
628+
# example of how to calculate:
629+
# shift so you rotate about origin (subtract center)
630+
# (0, 0) - (1, 2/3) = (-1, -2/3)
631+
# rotate 45 degrees clockwise = (-1 * cos(45) - (-2/3) * sin(45), (-2/3) * cos(45) + (-1) * sin(45)) = (-0.23570226039, -1.17851130198)
632+
# shift back (add center): (0.76429773961, -0.51184463531)
633+
self.assertAlmostEqual(0.76429773961, triangle_rot.points[0].x)
634+
self.assertAlmostEqual(-0.51184463531, triangle_rot.points[0].y)
635+
self.assertAlmostEqual(1.23570226039, triangle_rot.points[1].x)
636+
self.assertAlmostEqual(0.90236892706, triangle_rot.points[1].y)
637+
self.assertAlmostEqual(0.47140452079, triangle_rot.points[2].x)
638+
self.assertAlmostEqual(1.60947570825, triangle_rot.points[2].y)
639+
self.assertAlmostEqual(1, triangle_rot.center.x)
640+
self.assertAlmostEqual(0.66666666667, triangle_rot.center.y)
641+
642+
643+
def test_area(self):
644+
# http://www.mathopenref.com/coordpolygonareacalc.html# helpful for checking
645+
poly = polygon2.Polygon2.from_regular(4, 1)
646+
self.assertAlmostEqual(1, poly.area)
647+
648+
poly2 = polygon2.Polygon2.from_regular(4, 2)
649+
self.assertAlmostEqual(4, poly2.area)
650+
651+
poly3 = polygon2.Polygon2.from_regular(8, 3.7)
652+
self.assertAlmostEqual(38.7, poly3.area)
653+
654+
poly4 = polygon2.Polygon2([ (0, 0), (1, 1), (2, 1) ])
655+
self.assertAlmostEqual(0.5, poly4.area)
656+
657+
poly5 = polygon2.Polygon2([ (0, 0), (1, 1), (2, 1), (1, -0.25) ])
658+
self.assertAlmostEqual(1.25, poly5.area)
659+
660+
def _proj_onto_axis_fuzzer(self, points, axis, expected):
661+
for i in range(3):
662+
offset = vector2.Vector2(random.randrange(-1000, 1000, 0.01), random.randrange(-1000, 1000, 0.01))
663+
664+
new_points = []
665+
for pt in points:
666+
new_points.append(pt - offset)
667+
668+
new_poly = polygon2.Polygon2(new_points)
669+
670+
proj = polygon2.Polygon2.project_onto_axis(new_poly, offset, axis)
671+
672+
help_msg = "points={}, axis={}, expected={} proj={} [offset = {}, new_points={}]".format(points, axis, expected, proj, offset, new_points)
673+
self.assertAlmostEqual(expected.min, proj.min, help_msg)
674+
self.assertAlmostEqual(expected.max, proj.max, help_msg)
675+
676+
677+
def test_project_onto_axis(self):
678+
poly = polygon2.Polygon2.from_regular(4, 1, math.pi / 4)
529679

680+
_axis = vector2.Vector2(0, 1)
681+
self._proj_onto_axis_fuzzer(poly.points, _axis, axisall.AxisAlignedLine(_axis, 0, 1))
682+
683+
_axis2 = vector2.Vector2(1, 0)
684+
self._proj_onto_axis_fuzzer(poly.points, _axis2, axisall.AxisAlignedLine(_axis2, 0, 1))
685+
686+
_axis3 = vector2.Vector2(0.70710678118, 0.70710678118)
687+
self._proj_onto_axis_fuzzer(poly.points, _axis3, axisall.AxisAlignedLine(_axis3, 0, 1.41421356236))
688+
689+
690+
def test_contains_point_false(self):
691+
pass
692+
693+
def test_contains_point_edge(self):
694+
pass
695+
696+
def test_contains_point_contained(self):
697+
pass
698+
699+
def test_find_intersection_false(self):
700+
pass
701+
702+
def test_find_intersection_touching(self):
703+
pass
704+
705+
def test_find_intersection_overlapping(self):
706+
pass
530707

531708
if __name__ == '__main__':
532709
unittest.main()

0 commit comments

Comments
 (0)