示例#1
0
    def test_think(self):
        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(15, 15)))
        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(20, 20)))
        ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0,
                                                                         0)))
        ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(5,
                                                                         0)))
        ent5 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0,
                                                                         5)))
        _tree = quadtree.QuadTree(2,
                                  2,
                                  self.big_rect,
                                  entities=[ent1, ent2, ent3, ent4, ent5])

        _tree.think(True)

        self.assertIsNotNone(_tree.children)  # depth 1
        self.assertIsNotNone(_tree.children[0].children)  # depth 2
        self.assertIsNone(_tree.children[0].children[0].children
                          )  # depth 3 shouldn't happen because
        self.assertEqual(5, len(
            _tree.children[0].children[0].entities))  # max_depth reached

        _tree2 = quadtree.QuadTree(2, 2, self.big_rect, entities=[ent1, ent2])
        _tree2.think(True)
        self.assertIsNone(_tree2.children)
示例#2
0
 def setUp(self):
     self.big_rect = rect2.Rect2(1000, 1000)
     self.big_rect_sub_1 = rect2.Rect2(500, 500)
     self.big_rect_sub_2 = rect2.Rect2(500, 500, vector2.Vector2(500, 0))
     self.big_rect_sub_3 = rect2.Rect2(500, 500, vector2.Vector2(500, 500))
     self.big_rect_sub_4 = rect2.Rect2(500, 500, vector2.Vector2(0, 500))
     random.seed()
    def split(self):
        """
        Split this quadtree.
        
        .. caution::
            
            A call to split will always split the tree or raise an error. Use 
            :py:meth:`.think` if you want to ensure the quadtree is operating
            efficiently.
            
        .. caution::
            
            This function will not respect :py:attr:`.bucket_size` or 
            :py:attr:`.max_depth`.
            
        :raises ValueError: if :py:attr:`.children` is not empty
        """
        if self.children:
            raise ValueError("cannot split twice")

        _cls = type(self)

        def _cstr(r):
            return _cls(self.bucket_size, self.max_depth, r, self.depth + 1)

        _halfwidth = self.location.width / 2
        _halfheight = self.location.height / 2
        _x = self.location.mincorner.x
        _y = self.location.mincorner.y

        self.children = [
            _cstr(rect2.Rect2(_halfwidth, _halfheight, vector2.Vector2(_x,
                                                                       _y))),
            _cstr(
                rect2.Rect2(_halfwidth, _halfheight,
                            vector2.Vector2(_x + _halfwidth, _y))),
            _cstr(
                rect2.Rect2(_halfwidth, _halfheight,
                            vector2.Vector2(_x + _halfwidth,
                                            _y + _halfheight))),
            _cstr(
                rect2.Rect2(_halfwidth, _halfheight,
                            vector2.Vector2(_x, _y + _halfheight)))
        ]

        _newents = []
        for ent in self.entities:
            quad = self.get_quadrant(ent)

            if quad < 0:
                _newents.append(ent)
            else:
                self.children[quad].entities.append(ent)
        self.entities = _newents
示例#4
0
    def test_split_entities(self):

        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(50, 50)))
        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(550, 75)))
        ent3 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(565, 585)))
        ent4 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(95, 900)))
        ent5 = quadtree.QuadTreeEntity(
            rect2.Rect2(10, 10, vector2.Vector2(495, 167)))

        _tree = quadtree.QuadTree(64,
                                  5,
                                  self.big_rect,
                                  entities=[ent1, ent2, ent3, ent4, ent5])
        _tree.split()

        self.assertEqual(1, len(_tree.children[0].entities))
        self.assertEqual(50, _tree.children[0].entities[0].aabb.mincorner.x)
        self.assertEqual(50, _tree.children[0].entities[0].aabb.mincorner.y)

        self.assertEqual(1, len(_tree.children[1].entities))
        self.assertEqual(550, _tree.children[1].entities[0].aabb.mincorner.x)
        self.assertEqual(75, _tree.children[1].entities[0].aabb.mincorner.y)

        self.assertEqual(1, len(_tree.children[2].entities))
        self.assertEqual(565, _tree.children[2].entities[0].aabb.mincorner.x)
        self.assertEqual(585, _tree.children[2].entities[0].aabb.mincorner.y)

        self.assertEqual(1, len(_tree.children[3].entities))
        self.assertEqual(95, _tree.children[3].entities[0].aabb.mincorner.x)
        self.assertEqual(900, _tree.children[3].entities[0].aabb.mincorner.y)

        self.assertEqual(1, len(_tree.entities))
        self.assertEqual(495, _tree.entities[0].aabb.mincorner.x)
        self.assertEqual(167, _tree.entities[0].aabb.mincorner.y)

        _tree2 = _tree.children[3]
        _tree2.split()

        for i in range(3):
            self.assertEqual(0,
                             len(_tree2.children[i].entities),
                             msg="i={}".format(i))

        self.assertEqual(1, len(_tree2.children[3].entities))
        self.assertEqual(95, _tree2.children[3].entities[0].aabb.mincorner.x)
        self.assertEqual(900, _tree2.children[3].entities[0].aabb.mincorner.y)
示例#5
0
            def unshift_vec(vec):
                numerator = line1.slope * vec.x - yintr1 * axis.x * axis.x
                denominator = axis.x * axis.y + line1.slope * axis.y * axis.y

                new_x = numerator / denominator
                new_y = line1.slope * new_x + yintr1

                return vector2.Vector2(new_x, new_y)
示例#6
0
 def test_nodes_per_depth(self):
     _tree = quadtree.QuadTree(1, 5, self.big_rect)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(50,
                                                                   50))))
     self.assertDictEqual({0: 1}, _tree.find_nodes_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(450, 450))))
     self.assertDictEqual({0: 1, 1: 4, 2: 4}, _tree.find_nodes_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(550, 550))))
     self.assertDictEqual({0: 1, 1: 4, 2: 4}, _tree.find_nodes_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(850, 550))))
     self.assertDictEqual({0: 1, 1: 4, 2: 8}, _tree.find_nodes_per_depth())
示例#7
0
    def test_repr(self):
        _tree = quadtree.QuadTree(1, 5, rect2.Rect2(100, 100))

        _tree.insert_and_think(
            quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5))))
        _tree.insert_and_think(
            quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5))))

        _olddiff = self.maxDiff

        def cleanup(self2=self):
            self2.maxDiff = _olddiff

        self.addCleanup(cleanup)
        self.maxDiff = None
        self.assertEqual(
            "quadtree(bucket_size=1, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=0, y=0)), depth=1, entities=[quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5)))], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=50.0, y=0)), depth=1, entities=[quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5)))], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=50.0, y=50.0)), depth=1, entities=[], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=0, y=50.0)), depth=1, entities=[], children=None)])",
            repr(_tree))
示例#8
0
 def test_insert(self):
     _tree = quadtree.QuadTree(2, 2, self.big_rect)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(15,
                                                                   15))))
     self.assertIsNone(_tree.children)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(20,
                                                                   20))))
     self.assertIsNone(_tree.children)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0, 0))))
     self.assertIsNotNone(_tree.children)  # depth 1
     self.assertIsNotNone(_tree.children[0].children)  # depth 2
     self.assertIsNone(_tree.children[0].children[0].children
                       )  # depth 3 shouldn't happen because
     self.assertEqual(3, len(
         _tree.children[0].children[0].entities))  # max_depth reached
示例#9
0
    def test_str(self):
        _tree = quadtree.QuadTree(1, 5, rect2.Rect2(100, 100))

        _tree.insert_and_think(
            quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5))))
        _tree.insert_and_think(
            quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5))))

        _olddiff = self.maxDiff

        def cleanup(self2=self):
            self2.maxDiff = _olddiff

        self.addCleanup(cleanup)
        self.maxDiff = None
        self.assertEqual(
            "quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 5, actual: 1), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad)",
            str(_tree))
示例#10
0
    def test_get_quadrant_shifted(self):
        _tree = quadtree.QuadTree(64, 5, self.big_rect_sub_3)

        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(515, 600)))
        self.assertEqual(0, _tree.get_quadrant(ent1))

        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(800, 550)))
        self.assertEqual(1, _tree.get_quadrant(ent2))

        ent3 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(950, 850)))
        self.assertEqual(2, _tree.get_quadrant(ent3))

        ent4 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(515, 751)))
        self.assertEqual(3, _tree.get_quadrant(ent4))
    def from_rotated(cls, original, rotation, rotation_degrees=None):
        """
        Create a regular polygon that is a rotation of
        a different polygon.
        
        The rotation must be in radians, or null and rotation_degrees
        must be specified. Positive rotations are clockwise.
        
        Examples:
        
        .. code-block:: python
        
            from pygorithm.goemetry import (vector2, polygon2)
            import math
            
            poly = polygon2.Polygon2.from_regular(4, 1)
            
            # the following are equivalent (within rounding)
            rotated1 = polygon2.Polygon2.from_rotated(poly, math.pi / 4)
            rotated2 = polygon2.Polygon2.from_rotated(poly, None, 45)
        
        Uses the `2-d rotation matrix <https://en.wikipedia.org/wiki/Rotation_matrix>`
        to rotate each point.
        
        :param original: the polygon to rotate
        :type original: :class:`pygorithm.geometry.polygon2.Polygon2`
        :param rotation: the rotation in radians or None
        :type rotation: :class:`numbers.Number`
        :param rotation_degrees: the rotation in degrees or None
        :type rotation_degrees: :class:`numbers.Number`
        :returns: the rotated polygon
        :rtype: :class:`pygorithm.geometry.polygon2.Polygon2`
        
        :raises ValueError: if ``rotation is not None and rotation_degrees is not None``
        :raises ValueError: if ``rotation is None and rotation_degrees is None``
        """
        if (rotation is None) == (rotation_degrees is None):
            raise ValueError(
                "rotation must be specified exactly once (rotation={}, rotation_degrees={})"
                .format(rotation, rotation_degrees))

        if rotation_degrees is not None:
            rotation = rotation_degrees * math.pi / 180

        new_pts = []
        for pt in original.points:
            shifted = pt - original.center
            new_pts.append(
                vector2.Vector2(
                    original.center.x + shifted.x * math.cos(rotation) -
                    shifted.y * math.sin(rotation),
                    original.center.y + shifted.y * math.cos(rotation) +
                    shifted.x * math.sin(rotation)))

        result = cls(new_pts, suppress_errors=True)
        result._area = original._area
        return result
示例#12
0
 def test_ents_per_depth(self):
     _tree = quadtree.QuadTree(3, 5, self.big_rect)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75,
                                                                   35))))
     self.assertDictEqual({0: 1}, _tree.find_entities_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(300, 499))))
     self.assertDictEqual({0: 2}, _tree.find_entities_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(800, 600))))
     self.assertDictEqual({0: 3}, _tree.find_entities_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(450, 300))))
     self.assertDictEqual({0: 1, 1: 3}, _tree.find_entities_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(150, 100))))
     self.assertDictEqual({0: 1, 1: 4}, _tree.find_entities_per_depth())
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(80,
                                                                   40))))
     self.assertDictEqual({
         0: 1,
         1: 1,
         2: 4
     }, _tree.find_entities_per_depth())
示例#13
0
 def test_avg_ents_per_leaf(self):
     _tree = quadtree.QuadTree(3, 5, self.big_rect)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75,
                                                                   35))))
     self.assertEqual(
         1, _tree.calculate_avg_ents_per_leaf())  # 1 ent on 1 leaf
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(300, 499))))
     self.assertEqual(2,
                      _tree.calculate_avg_ents_per_leaf())  # 2 ents 1 leaf
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(800, 600))))
     self.assertEqual(3,
                      _tree.calculate_avg_ents_per_leaf())  # 3 ents 1 leaf
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(450, 300))))
     self.assertEqual(0.75, _tree.calculate_avg_ents_per_leaf()
                      )  # 3 ents 4 leafs (1 misplaced)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(150, 100))))
     self.assertEqual(1, _tree.calculate_avg_ents_per_leaf()
                      )  # 4 ents 4 leafs (1 misplaced)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(450, 450))))
     self.assertAlmostEqual(5 / 7, _tree.calculate_avg_ents_per_leaf()
                            )  # 5 ents 7 leafs (1 misplaced)
示例#14
0
 def test_misplaced_ents(self):
     _tree = quadtree.QuadTree(3, 5, self.big_rect)
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75,
                                                                   35))))
     self.assertEqual(
         0, _tree.calculate_weight_misplaced_ents())  # 0 misplaced, 1 total
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(300, 499))))
     self.assertEqual(
         0, _tree.calculate_weight_misplaced_ents())  # 0 misplaced, 2 total
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(800, 600))))
     self.assertEqual(
         0, _tree.calculate_weight_misplaced_ents())  # 0 misplaced 3 total
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(550, 700))))
     self.assertAlmostEqual(1, _tree.calculate_weight_misplaced_ents()
                            )  # 1 misplaced (1 deep), 4 total
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(900, 900))))
     self.assertAlmostEqual(4 / 5, _tree.calculate_weight_misplaced_ents()
                            )  # 1 misplaced (1 deep), 5 total
     _tree.insert_and_think(
         quadtree.QuadTreeEntity(
             rect2.Rect2(5, 5, vector2.Vector2(950, 950))))
     self.assertAlmostEqual(8 / 6, _tree.calculate_weight_misplaced_ents()
                            )  # 1 misplaced (2 deep), 6 total
示例#15
0
    def test_get_quadrant(self):
        _tree = quadtree.QuadTree(64, 5, self.big_rect)

        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(320, 175)))
        quad1 = _tree.get_quadrant(ent1)
        self.assertEqual(0, quad1)

        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(600, 450)))
        quad2 = _tree.get_quadrant(ent2)
        self.assertEqual(1, quad2)

        ent3 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(700, 950)))
        quad3 = _tree.get_quadrant(ent3)
        self.assertEqual(2, quad3)

        ent4 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(0, 505)))
        quad4 = _tree.get_quadrant(ent4)
        self.assertEqual(3, quad4)
示例#16
0
    def test_get_quadrant_none(self):
        _tree = quadtree.QuadTree(64, 5, self.big_rect)

        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(497, 150)))
        self.assertEqual(-1, _tree.get_quadrant(ent1))

        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(800, 499)))
        self.assertEqual(-1, _tree.get_quadrant(ent2))

        ent3 = quadtree.QuadTreeEntity(
            rect2.Rect2(15, 15, vector2.Vector2(486, 505)))
        self.assertEqual(-1, _tree.get_quadrant(ent3))

        ent4 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 20, vector2.Vector2(15, 490)))
        self.assertEqual(-1, _tree.get_quadrant(ent4))

        ent5 = quadtree.QuadTreeEntity(
            rect2.Rect2(17, 34, vector2.Vector2(485, 470)))
        self.assertEqual(-1, _tree.get_quadrant(ent5))
    def polygon(self):
        """
        Get the polygon representation of this rectangle, without
        the offset. Lazily initialized and up-to-date with width 
        and height.
        
        .. caution::
        
            This does not include the :py:attr:`.mincorner`
            (which should be passed as offset for polygon operations)
        
        :returns: polygon representation of this rectangle
        :rtype: :class:`pygorithm.geometry.polygon2.Polygon2`
        """
        if self._polygon is None:
            self._polygon = polygon2.Polygon2([
                vector2.Vector2(0, 0),
                vector2.Vector2(0, self._height),
                vector2.Vector2(self._width, self._height),
                vector2.Vector2(self._width, 0)
            ])

        return self._polygon
    def project_onto_axis(rect, axis):
        """
        Project the rect onto the specified axis.
        
        .. tip::
        
            This function is extremely fast for vertical or 
            horizontal axises.
        
        :param rect: the rect to project
        :type rect: :class:`pygorithm.geometry.rect2.Rect2`
        :param axis: the axis to project onto (normalized)
        :type axis: :class:`pygorithm.geometry.vector2.Vector2`
        :returns: the projection of the rect along axis
        :rtype: :class:`pygorithm.geometry.axisall.AxisAlignedLine`
        """

        if axis.x == 0:
            return axisall.AxisAlignedLine(axis, rect.mincorner.y * axis.y,
                                           (rect.mincorner.y + rect.height) *
                                           axis.y)
        elif axis.y == 0:
            return axisall.AxisAlignedLine(axis, rect.mincorner.x * axis.x,
                                           (rect.mincorner.x + rect.width) *
                                           axis.x)

        p1 = rect.mincorner.dot(axis)
        p2 = vector2.Vector2(rect.mincorner.x + rect.width,
                             rect.mincorner.y).dot(axis)
        p3 = vector2.Vector2(rect.mincorner.x + rect.width,
                             rect.mincorner.y + rect.height).dot(axis)
        p4 = vector2.Vector2(rect.mincorner.x,
                             rect.mincorner.y + rect.height).dot(axis)

        _min = min(p1, p2, p3, p4)
        _max = max(p1, p2, p3, p4)
        return axisall.AxisAlignedLine(axis, _min, _max)
示例#19
0
    def normal(self):
        """
        Get normalized normal vector to axis, lazily initialized.

        Get the normalized normal vector such that the normal
        vector is 90 degrees counter-clockwise from the axis.
        
        :returns: normalized normal to axis
        :rtype: :class:`pygorithm.geometry.vector2.Vector2`
        """

        if self._normal is None:
            self._normal = vector2.Vector2(-self.axis.y, self.axis.x)

        return self._normal
 def __init__(self, width, height, mincorner=None):
     """
     Create a new rectangle of width and height.
     
     If ``mincorner is None``, the origin is assumed.
     
     :param width: width of this rect
     :type width: :class:`numbers.Number`
     :param height: height of this rect
     :type height: :class:`numbers.Number`
     :param mincorner: the position of this rect
     :type mincorner: :class:`pygorithm.geometry.vector2.Vector2` or None
     
     :raises ValueError: if width or height are not strictly positive
     """
     self.width = width
     self.height = height
     self.mincorner = mincorner if mincorner is not None else vector2.Vector2(
         0, 0)
示例#21
0
    def test_sum_ents(self):
        # it shouldn't matter where we put entities in, adding entities
        # to a quadtree should increment this number by 1. So lets fuzz!

        _tree = quadtree.QuadTree(64, 5, self.big_rect)
        for i in range(1000):
            w = random.randrange(1, 10)
            h = random.randrange(1, 10)
            x = random.uniform(0, 1000 - w)
            y = random.uniform(0, 1000 - h)
            ent = quadtree.QuadTreeEntity(
                rect2.Rect2(w, h, vector2.Vector2(x, y)))
            _tree.insert_and_think(ent)

            # avoid calculating sum every loop which would take way too long.
            # on average, try to sum about 50 times total (5% of the time),
            # evenly split between both ways of summing
            rnd = random.random()
            if rnd > 0.95 and rnd <= 0.975:
                _sum = _tree.sum_entities()
                self.assertEqual(i + 1, _sum)
            elif rnd > 0.975:
                _sum = _tree.sum_entities(_tree.find_entities_per_depth())
                self.assertEqual(i + 1, _sum)
示例#22
0
    def test_get_quadrant_0_shifted(self):
        _tree = quadtree.QuadTree(
            64, 5, rect2.Rect2(500, 800, vector2.Vector2(200, 200)))

        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 10, vector2.Vector2(445, 224)))
        self.assertEqual(-1, _tree.get_quadrant(ent1))

        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(11, 17, vector2.Vector2(515, 585)))
        self.assertEqual(-1, _tree.get_quadrant(ent2))

        ent3 = quadtree.QuadTreeEntity(
            rect2.Rect2(20, 20, vector2.Vector2(440, 700)))
        self.assertEqual(-1, _tree.get_quadrant(ent3))

        ent4 = quadtree.QuadTreeEntity(
            rect2.Rect2(15, 15, vector2.Vector2(215, 590)))
        self.assertEqual(-1, _tree.get_quadrant(ent4))

        ent5 = quadtree.QuadTreeEntity(
            rect2.Rect2(7, 12, vector2.Vector2(449, 589)))
        self.assertEqual(-1, _tree.get_quadrant(ent5))
示例#23
0
 def setUp(self):
     self.rect1 = rect2.Rect2(1, 1, vector2.Vector2(2, 2))
示例#24
0
    def find_intersection(line1, line2, offset1=None, offset2=None):
        """
        Find the intersection between the two lines.
        
        The lines may optionally be offset by a fixed amount. This 
        will incur a minor performance penalty which is less than 
        that of recreating new lines.
        
        Two lines are considered touching if they only share exactly
        one point and that point is an edge of one of the lines. 
        
        If two lines are parallel, their intersection could be a line.
        
        .. tip::
        
            This will never return True, True
        
        :param line1: the first line
        :type line1: :class:`pygorithm.geometry.line2.Line2`
        :param line2: the second line
        :type line2: :class:`pygorithm.geometry.line2.Line2`
        :param offset1: the offset of line 1
        :type offset1: :class:`pygorithm.geometry.vector2.Vector2` or None
        :param offset2: the offset of line 2
        :type offset2: :class:`pygorithm.geometry.vector2.Vector2` or None
        :returns: (touching, overlapping, intersection_location)
        :rtype: (bool, bool, :class:`pygorithm.geometry.line2.Line2` or :class:`pygorithm.geometry.vector2.Vector2` or None)
        """

        # We will ensure that:
        #  - If one line is vertical and one horizontal, line1 is the vertical line
        #  - If only one line is vertical, line1 is the vertical line
        #  - If only one line is horizontal, line1 is the horizontal line

        if line2.vertical and not line1.vertical:
            return Line2.find_intersection(line2, line1, offset2, offset1)
        if line2.horizontal and not line1.horizontal and not line1.vertical:
            return Line2.find_intersection(line2, line1, offset2, offset1)

        l1_st_x = line1.start.x + (offset1.x if offset1 is not None else 0)
        l1_st_y = line1.start.y + (offset1.y if offset1 is not None else 0)
        l1_en_x = line1.end.x + (offset1.x if offset1 is not None else 0)
        l1_en_y = line1.end.y + (offset1.y if offset1 is not None else 0)

        l2_st_x = line2.start.x + (offset2.x if offset2 is not None else 0)
        l2_st_y = line2.start.y + (offset2.y if offset2 is not None else 0)
        l2_en_x = line2.end.x + (offset2.x if offset2 is not None else 0)
        l2_en_y = line2.end.y + (offset2.y if offset2 is not None else 0)

        if line1.vertical and line2.vertical:
            # Two vertical lines
            if not math.isclose(l1_st_x, l2_st_x):
                return False, False, None

            aal1 = axisall.AxisAlignedLine(None, l1_st_y, l1_en_y)
            aal2 = axisall.AxisAlignedLine(None, l2_st_y, l2_en_y)

            touch, mtv = axisall.AxisAlignedLine.find_intersection(aal1, aal2)

            if not touch:
                return False, False, None
            elif mtv[0] is None:
                return True, False, vector2.Vector2(l1_st_x, mtv[1])
            else:
                return False, True, Line2(vector2.Vector2(l1_st_x, mtv[1]),
                                          vector2.Vector2(l1_st_x, mtv[2]))

        if line1.horizontal and line2.horizontal:
            # Two horizontal lines
            if not math.isclose(l1_st_y, l2_st_y):
                return False, False, None

            aal1 = axisall.AxisAlignedLine(None, l1_st_x, l1_en_x)
            aal2 = axisall.AxisAlignedLine(None, l2_st_x, l2_st_y)

            touch, mtv = axisall.AxisAlignedLine.find_intersection(aal1, aal2)

            if not touch:
                return False, False, None
            elif mtv[0] is None:
                return True, False, vector2.Vector2(mtv[1], l1_st_y)
            else:
                return False, True, Line2(vector2.Vector2(mtv[1], l1_st_x),
                                          vector2.Vector2(mtv[2], l1_st_y))

        if Line2.are_parallel(line1, line2):
            # Two non-vertical, non-horizontal, parallel lines
            yintr1 = line1.calculate_y_intercept(offset1)
            yintr2 = line2.calculate_y_intercept(offset2)
            if not math.isclose(yintr1, yintr2):
                return False, False, None

            axis = line1.axis
            aal1 = axisall.AxisAlignedLine(axis,
                                           l1_st_x * axis.x + l1_st_y * axis.y,
                                           l1_en_x * axis.x + l1_en_y * axis.y)
            aal2 = axisall.AxisAlignedLine(axis,
                                           l2_st_x * axis.x + l2_st_y * axis.y,
                                           l2_en_x * axis.x + l2_en_y * axis.y)

            touch, mtv = axisall.AxisAlignedLine.find_intersection(aal1, aal2)

            def unshift_vec(vec):
                numerator = line1.slope * vec.x - yintr1 * axis.x * axis.x
                denominator = axis.x * axis.y + line1.slope * axis.y * axis.y

                new_x = numerator / denominator
                new_y = line1.slope * new_x + yintr1

                return vector2.Vector2(new_x, new_y)

            if not touch:
                return False, False, None
            elif mtv[0] is None:
                return True, False, unshift_vec(axis * mtv[1])
            else:
                return False, True, Line2(unshift_vec(axis * mtv[1]),
                                          unshift_vec(axis * mtv[2]))

        if line1.vertical and line2.horizontal:
            # A vertical and horizontal line
            l1_min = min(l1_st_y,
                         l1_en_y) if offset1 is not None else line1.min_y
            l1_max = max(l1_st_y,
                         l1_en_y) if offset1 is not None else line1.max_y

            if l2_st_y < l1_min or l2_st_y > l2_max:
                return False, False, None

            l2_min = min(l2_st_x,
                         l2_en_x) if offset2 is not None else line2.min_x
            l2_max = max(l2_st_x,
                         l2_en_x) if offset2 is not None else line2.max_x

            if l1_st_x < l2_min or l1_st_x > l2_max:
                return False, False, None

            pt = vector2.Vector2(l1_st_x, l2_st_y)

            if math.isclose(l2_st_y, l1_min) or math.isclose(
                    l2_st_y, l2_max) or math.isclose(
                        l1_st_x, l2_min) or math.isclose(l2_st_y, l2_max):
                return True, False, pt
            else:
                return False, True, pt

        if line1.vertical:
            # A vertical and non-horizontal, non-vertical line
            line2_y_at_line1_x = line2.slope * l1_st_x + line2.calculate_y_intercept(
                offset2)

            l1_min = min(l1_st_y,
                         l1_en_y) if offset1 is not None else line1.min_y
            l1_max = max(l1_st_y,
                         l1_en_y) if offset1 is not None else line1.max_y

            if math.isclose(line2_y_at_line1_x, l1_min) or math.isclose(
                    line2_y_at_line1_x, l1_max):
                return True, False, vector2.Vector2(l1_st_x,
                                                    line2_y_at_line1_x)
            elif line2_y_at_line1_x < l1_min or line2_y_at_line1_x > l2_max:
                return False, False, None
            else:
                return False, True, vector2.Vector2(l1_st_x,
                                                    line2_y_at_line1_x)

        if line1.horizontal:
            # A horizontal and non-vertical, non-horizontal line
            # y = mx + b -> x = (y - b) / m
            line2_x_at_line1_y = (
                l1_st_y - line2.calculate_y_intercept(offset2)) / line2.slope

            l1_min = min(l1_st_x,
                         l1_en_x) if offset1 is not None else line1.min_x
            l1_max = max(l1_st_x,
                         l1_en_x) if offset1 is not None else line1.max_x

            if math.isclose(line2_x_at_line1_y, l1_min) or math.isclose(
                    line2_x_at_line1_y, l1_max):
                return True, False, vector2.Vector2(line2_x_at_line1_y,
                                                    l1_st_y)
            elif line2_x_at_line1_y < l1_min or line2_x_at_line1_y > l1_max:
                return False, False, None
            else:
                return False, True, vector2.Vector2(line2_x_at_line1_y,
                                                    l1_st_y)

        # Two non-vertical, non-horizontal, non-parallel lines

        # y = m1 x + b1
        # y = m2 x + b2
        # m1 x + b1 = m2 x + b2
        # m1 x - m2 x = b2 - b1
        # x = (b2 - b1) / (m1 - m2)

        yintr1 = line1.calculate_y_intercept(offset1)
        yintr2 = line2.calculate_y_intercept(offset2)
        intr_x = (yintr2 - yintr1) / (line1.slope - line2.slope)

        # Some caution needs to be taken here to ensure we do approximately before range
        # checks. It's possible for _approx(a, b) to be True and a < b to be True

        on_edge1 = math.isclose(intr_x, l1_st_x) or math.isclose(
            intr_x, l1_en_x)
        on_edge2 = math.isclose(intr_x, l2_st_x) or math.isclose(
            intr_x, l2_en_x)

        if on_edge1 and on_edge2:
            intr_y = line1.slope * intr_x + yintr1
            return True, False, vector2.Vector2(intr_x, intr_y)

        l1_min_x = min(l1_st_x,
                       l1_en_x) if offset1 is not None else line1.min_x
        l1_max_x = max(l1_st_x,
                       l1_en_x) if offset1 is not None else line1.max_x
        l2_min_x = min(l2_st_x,
                       l2_en_x) if offset2 is not None else line2.min_x
        l2_max_x = max(l2_st_x,
                       l2_en_x) if offset2 is not None else line2.max_x

        on_line1 = on_edge1 or (intr_x > l1_min_x and intr_x < l1_max_x)
        on_line2 = on_edge2 or (intr_x > l2_min_x and intr_x < l2_max_x)

        if on_line1 and on_line2:
            intr_y = line1.slope * intr_x + yintr1
            is_edge = on_edge1 or on_edge2
            return is_edge, not is_edge, vector2.Vector2(intr_x, intr_y)

        return False, False, None
    def _find_intersection_rects(cls, rect1, rect2, find_mtv=True):
        """
        Find the intersection between two rectangles. 
        
        Not intended for direct use. See 
        :py:meth:`.find_intersection`
        
        :param rect1: first rectangle
        :type rect1: :class:`pygorithm.geometry.rect2.Rect2`
        :param rect2: second rectangle
        :type rect2: :class:`pygorithm.geometry.rect2.Rect2`
        :param find_mtv: False to never find mtv (may allow small performance improvement)
        :type find_mtv: bool
        :returns: (touching, overlapping, (mtv distance, mtv axis))
        :rtype: (bool, bool, (:class:`numbers.Number`, :class:`pygorithm.geometry.vector2.Vector2`) or None)
        """

        # caution to make sure isclose checks are before greater than/less than checks!

        # you could save which edge here if you needed that information
        x_touching = math.isclose(rect1.mincorner.x + rect1.width,
                                  rect2.mincorner.x,
                                  abs_tol=1e-07)
        x_touching = x_touching or math.isclose(
            rect1.mincorner.x, rect2.mincorner.x + rect2.width, abs_tol=1e-07)
        y_touching = math.isclose(rect1.mincorner.y,
                                  rect2.mincorner.y + rect2.height,
                                  abs_tol=1e-07)
        y_touching = y_touching or math.isclose(
            rect1.mincorner.y + rect1.height, rect2.mincorner.y, abs_tol=1e-07)

        if x_touching and y_touching:
            return True, False, None  # sharing 1 corner

        # we don't need to calculate if the touching is True
        x_overlap = False if x_touching else (rect1.mincorner.x < rect2.mincorner.x and rect1.mincorner.x + rect1.width > rect2.mincorner.x) or \
                                             (rect2.mincorner.x < rect1.mincorner.x and rect2.mincorner.x + rect2.width > rect1.mincorner.x)
        y_overlap = False if y_touching else (rect1.mincorner.y < rect2.mincorner.y and rect1.mincorner.y + rect1.height > rect2.mincorner.y) or \
                                             (rect2.mincorner.y < rect1.mincorner.y and rect2.mincorner.y + rect2.height > rect1.mincorner.y)
        if x_touching:
            if y_overlap:
                return True, False, None  # sharing an x edge
            else:
                return False, False, None
        elif y_touching:
            if x_overlap:
                return True, False, None  # sharing a y edge
            else:
                return False, False, None
        elif not x_overlap or not y_overlap:
            return False, False, None

        # They overlap
        if not find_mtv:
            return False, True, None

        # four options:
        #   move rect1 min x to rect2 max x
        #   move rect1 max x to rect2 min x
        #   move rect1 min y to rect2 max y
        #   move rect1 max y to rect2 min y
        #
        # we will look at all 4 of these and choose
        # the one that requires the least movement
        opt1 = rect2.mincorner.x + rect2.width - rect1.mincorner.x
        opt2 = rect2.mincorner.x - rect1.mincorner.x - rect1.width
        opt3 = rect2.mincorner.y + rect2.height - rect1.mincorner.y
        opt4 = rect2.mincorner.y - rect1.mincorner.y - rect1.height

        abs1 = abs(opt1)
        abs2 = abs(opt2)
        abs3 = abs(opt3)
        abs4 = abs(opt4)
        # the following could be simplified by making an array, at a
        # minor performance hit
        if abs1 < abs2:
            if abs1 < abs3:
                if abs1 < abs4:
                    return False, True, (opt1, vector2.Vector2(1, 0))
                else:
                    return False, True, (opt4, vector2.Vector2(0, 1))
            else:
                if abs3 < abs4:
                    return False, True, (opt3, vector2.Vector2(0, 1))
                else:
                    return False, True, (opt4, vector2.Vector2(0, 1))
        else:
            if abs2 < abs3:
                if abs2 < abs4:
                    return False, True, (opt2, vector2.Vector2(1, 0))
                else:
                    return False, True, (opt4, vector2.Vector2(0, 1))
            else:
                if abs3 < abs4:
                    return False, True, (opt3, vector2.Vector2(0, 1))
                else:
                    return False, True, (opt4, vector2.Vector2(0, 1))
    def __init__(self, points, suppress_errors=False):
        """
        Create a new polygon from the set of points
        
        .. caution:: 
        
            A significant amount of calculation is performed when creating
            a polygon. These should be reused whenever possible. This cost
            can be alleviated somewhat by suppressing certain expensive
            sanity checks, but the polygon can behave very unexpectedly
            (and potentially without explicit errors) if the errors are 
            suppressed.
        
        The center of the polygon is calculated as the average of the points.
        
        The lines of the polygon are constructed using line2. 
        
        The normals of the lines are calculated using line2.
        
        A simple linear search is done to check for repeated points.
        
        The area is calculated to check for clockwise order using the
        `Shoelace Formula <https://en.wikipedia.org/wiki/Shoelace_formula>`
        
        The polygon is proven to be convex by ensuring the cross product of 
        the line from the point to previous point and point to next point is
        positive or 0, for all points.
        
        :param points: the ordered set of points on this polygon
        :type points: list of :class:`pygorithm.geometry.vector2.Vector2` or \
        list of (:class:`numbers.Number`, :class:`numbers.Number`)
        
        :param suppress_errors: True to not do somewhat expensive sanity checks
        :type suppress_errors: bool
        
        :raises ValueError: if there are less than 3 points (not suppressable)
        :raises ValueError: if there are any repeated points (suppressable)
        :raises ValueError: if the points are not clockwise oriented (suppressable)
        :raises ValueError: if the polygon is not convex (suppressable)
        """
        if len(points) < 3:
            raise ValueError(
                "Not enough points (need at least 3 to define a polygon, got {}"
                .format(len(points)))

        self.points = []
        self.lines = []
        self.normals = []
        _sum = vector2.Vector2(0, 0)

        for pt in points:
            act_pt = pt if type(pt) == vector2.Vector2 else vector2.Vector2(pt)

            if not suppress_errors:
                for prev_pt in self.points:
                    if math.isclose(prev_pt.x, act_pt.x) and math.isclose(
                            prev_pt.y, act_pt.y):
                        raise ValueError(
                            'Repeated points! points={} (repeated={})'.format(
                                points, act_pt))

            _sum += act_pt
            self.points.append(act_pt)
        self.center = _sum * (1 / len(self.points))

        _previous = self.points[0]
        for i in range(1, len(self.points) + 1):
            pt = self.points[i % len(self.points)]
            _line = line2.Line2(_previous, pt)
            self.lines.append(_line)
            norm = vector2.Vector2(_line.normal)
            if norm.x < 0 or (norm.x == 0 and norm.y == -1):
                norm.x *= -1
                norm.y *= -1

            already_contains = next(
                (v for v in self.normals
                 if math.isclose(v.x, norm.x) and math.isclose(v.y, norm.y)),
                None)
            if already_contains is None:
                self.normals.append(norm)

            _previous = pt

        self._area = None

        if not suppress_errors:
            # this will check counter-clockwisedness
            a = self.area

            # if the polygon is convex and clockwise, if you look at any point
            # and take the cross product with the line from the point to the
            # previous point and the line from the point to the next point
            # the result will be positive
            for leftpointin in range(len(self.points)):
                middlepointin = (leftpointin + 1) % len(self.points)
                rightpointin = (middlepointin + 1) % len(self.points)

                leftpoint = self.points[leftpointin]
                middlepoint = self.points[middlepointin]
                rightpoint = self.points[rightpointin]

                vec1 = middlepoint - leftpoint
                vec2 = middlepoint - rightpoint
                cross_product = vec1.cross(vec2)
                if cross_product < -1e-09:
                    raise ValueError(
                        'Detected concavity at index {} - {} cross {} = {}\nself={}'
                        .format(middlepointin, vec1, vec2, cross_product,
                                str(self)))
    def from_regular(cls,
                     sides,
                     length,
                     start_rads=None,
                     start_degs=None,
                     center=None):
        """
        Create a new regular polygon.
        
        .. hint::
        
            If no rotation is specified there is always a point at ``(length, 0)``
        
        If no center is specified, the center will be calculated such that
        all the vertexes positive and the bounding box includes (0, 0). This
        operation requires O(n) time (where n is the number if sides)
        
        May specify the angle of the first point. For example, if the coordinate
        system is x to the right and y upward, then if the starting offset is 0
        then the first point will be at the right and the next point counter-clockwise.
        
        This would make for the regular quad (sides=4) to look like a diamond. To make
        the bottom side a square, the whole polygon needs to be rotated 45 degrees, like
        so:
        
        .. code-block:: python
        
            from pygorithm.geometry import (vector2, polygon2)
            import math
            
            # This is a diamond shape (rotated square) (0 degree rotation assumed)
            diamond = polygon2.Polygon2.from_regular(4, 1)
            
            # This is a flat square
            square = polygon2.Polygon2.from_regular(4, 1, start_degs = 45)
            
            # Creating a flat square with radians 
            square2 = polygon2.Polygon2.from_regular(4, 1, math.pi / 4)
        
        Uses the `definition of a regular polygon <https://en.wikipedia.org/wiki/Regular_polygon>`
        to find the angle between each vertex in the polygon. Then converts the side
        length to circumradius using the formula explained `here <http://mathworld.wolfram.com/RegularPolygon.html>`
        
        Finally, each vertex is found using ``<radius * cos(angle), radius * sin(angle)>``
        
        If the center is not specified, the minimum of the bounding box of the 
        polygon is calculated while the vertices are being found, and the inverse
        of that value is offset to the rest of the points in the polygon. 
        
        :param sides: the number of sides in the polygon
        :type sides: :class:`numbers.Number`
        :param length: the length of any side of the polygon
        :type length: :class:`numbers.Number`
        :param start_rads: the starting radians or None 
        :type start_rads: :class:`numbers.Number` or None
        :param start_degs: the starting degrees or None 
        :type start_degs: :class:`numbers.Number` or None
        :param center: the center of the polygon
        :type center: :class:`pygorithm.geometry.vector2.Vector2`
        :returns: the new regular polygon
        :rtype: :class:`pygorithm.geometry.polygon2.Polygon2`
        
        :raises ValueError: if ``sides < 3`` or ``length <= 0``
        :raises ValueError: if ``start_rads is not None and start_degs is not None``
        """

        if (start_rads is not None) and (start_degs is not None):
            raise ValueError(
                'One or neithter of start_rads and start_degs may be defined, but not both. (got start_rads={}, start_degs={})'
                .format(start_rads, start_degs))

        if sides < 3 or length <= 0:
            raise ValueError(
                'Too few sides or too non-positive length (sides={}, length={})'
                .format(sides, length))

        if start_degs is not None:
            start_rads = (start_degs * math.pi) / 180

        if start_rads is None:
            start_rads = 0

        _recenter = False
        radius = length / (2 * math.sin(math.pi / sides))
        if center is None:
            _recenter = True
            center = vector2.Vector2(0, 0)

        angle = start_rads
        increment = -(math.pi * 2) / sides

        pts = []
        _minx = 0
        _miny = 0
        for i in range(sides):
            x = center.x + math.cos(angle) * radius
            y = center.y + math.sin(angle) * radius
            pts.append(vector2.Vector2(x, y))
            angle += increment

            if _recenter:
                _minx = min(_minx, x)
                _miny = min(_miny, y)

        if _recenter:
            _offset = vector2.Vector2(-_minx, -_miny)
            for i in range(sides):
                pts[i] += _offset

        return cls(pts, suppress_errors=True)
示例#28
0
    def test_retrieve(self):
        _tree = quadtree.QuadTree(2, 2, self.big_rect)

        ent1 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(25, 25)))
        _tree.insert_and_think(ent1)

        retr = _tree.retrieve_collidables(ent1)
        self.assertIsNotNone(retr)
        self.assertEqual(1, len(retr))
        self.assertEqual(25, retr[0].aabb.mincorner.x)
        self.assertEqual(25, retr[0].aabb.mincorner.y)

        # note this is not nicely in a quadrant
        ent2 = quadtree.QuadTreeEntity(
            rect2.Rect2(20, 10, vector2.Vector2(490, 300)))
        _tree.insert_and_think(ent2)

        retr = _tree.retrieve_collidables(ent1)
        self.assertIsNotNone(retr)
        self.assertEqual(
            2,
            len(retr))  # both ent1 and ent2 are "collidable" in this quad tree

        # this should cause a split (bucket_size)
        ent3 = quadtree.QuadTreeEntity(
            rect2.Rect2(15, 10, vector2.Vector2(700, 450)))
        _tree.insert_and_think(ent3)

        ent4 = quadtree.QuadTreeEntity(
            rect2.Rect2(5, 5, vector2.Vector2(900, 900)))
        _tree.insert_and_think(ent4)

        # ent1 should collide with ent1 or ent2
        # ent2 with ent1 or ent2, or ent3
        # ent3 with ent2 or ent3
        # ent4 with ent2 or ent4
        retr = _tree.retrieve_collidables(ent1)
        self.assertIsNotNone(retr)
        self.assertEqual(2, len(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 25), None),
            str(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 490), None),
            str(retr))

        retr = _tree.retrieve_collidables(ent2)
        self.assertEqual(3, len(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 25), None),
            str(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 490), None),
            str(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 700), None),
            str(retr))

        retr = _tree.retrieve_collidables(ent3)
        self.assertEqual(2, len(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 490), None),
            str(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 700), None),
            str(retr))

        retr = _tree.retrieve_collidables(ent4)
        self.assertEqual(2, len(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 900), None),
            str(retr))
        self.assertIsNotNone(
            next((e for e in retr if e.aabb.mincorner.x == 490), None),
            str(retr))