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)
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
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)
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)
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())
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))
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
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))
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
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())
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)
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
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)
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)
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)
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)
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))
def setUp(self): self.rect1 = rect2.Rect2(1, 1, vector2.Vector2(2, 2))
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)
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))