From 86a6cc02835acdb52e498ce0ba69f31569332ff3 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 14 Jul 2024 15:00:13 +0200 Subject: [PATCH 1/8] split up geometry tests --- .../geometry/test_are_lines_intersecting.py | 33 +++++ .../test_are_polygons_intersecting.py | 19 +++ .../geometry/test_get_triangle_orientation.py | 12 ++ tests/unit/geometry/test_is_point_in_box.py | 21 ++++ .../unit/geometry/test_is_point_in_polygon.py | 37 ++++++ tests/unit/test_geometry.py | 118 ------------------ 6 files changed, 122 insertions(+), 118 deletions(-) create mode 100644 tests/unit/geometry/test_are_lines_intersecting.py create mode 100644 tests/unit/geometry/test_are_polygons_intersecting.py create mode 100644 tests/unit/geometry/test_get_triangle_orientation.py create mode 100644 tests/unit/geometry/test_is_point_in_box.py create mode 100644 tests/unit/geometry/test_is_point_in_polygon.py delete mode 100644 tests/unit/test_geometry.py diff --git a/tests/unit/geometry/test_are_lines_intersecting.py b/tests/unit/geometry/test_are_lines_intersecting.py new file mode 100644 index 0000000000..f35ac1941d --- /dev/null +++ b/tests/unit/geometry/test_are_lines_intersecting.py @@ -0,0 +1,33 @@ +from arcade.geometry import are_lines_intersecting + + +def test_are_lines_intersecting(): + line_a = [(0, 0), (50, 50)] + line_b = [(0, 0), (50, 50)] + assert are_lines_intersecting(*line_a, *line_b) is True + + # --------- + # Two lines clearly intersecting + line_a = [(0, 0), (50, 50)] + line_b = [(0, 50), (50, 0)] + assert are_lines_intersecting(*line_a, *line_b) is True + + # Two parallel lines clearly not intersecting + line_a = [(0, 0), (50, 0)] + line_b = [(0, 50), (0, 50)] + assert are_lines_intersecting(*line_a, *line_b) is False + + # Two lines intersecting at the edge points + line_a = [(0, 0), (50, 0)] + line_b = [(0, -50), (0, 50)] + assert are_lines_intersecting(*line_a, *line_b) is True + + # Twp perpendicular lines almost intersecting + line_a = [(0, 0), (50, 0)] + line_b = [(-1, -50), (-1, 50)] + assert are_lines_intersecting(*line_a, *line_b) is False + + # Twp perpendicular lines almost intersecting + line_a = [(0, 0), (50, 0)] + line_b = [(51, -50), (51, 50)] + assert are_lines_intersecting(*line_a, *line_b) is False diff --git a/tests/unit/geometry/test_are_polygons_intersecting.py b/tests/unit/geometry/test_are_polygons_intersecting.py new file mode 100644 index 0000000000..163c506990 --- /dev/null +++ b/tests/unit/geometry/test_are_polygons_intersecting.py @@ -0,0 +1,19 @@ +from arcade.geometry import are_polygons_intersecting + + +def test_are_polygons_intersecting(): + poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] + poly_b = [(25, 25), (25, 75), (75, 75), (75, 25)] + assert are_polygons_intersecting(poly_a, poly_b) is True + + +def test_are_empty_polygons_breaking(): + poly_a = [] + poly_b = [] + assert are_polygons_intersecting(poly_a, poly_b) is False + + +def test_are_mismatched_polygons_breaking(): + poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] + poly_b = [] + assert are_polygons_intersecting(poly_a, poly_b) is False diff --git a/tests/unit/geometry/test_get_triangle_orientation.py b/tests/unit/geometry/test_get_triangle_orientation.py new file mode 100644 index 0000000000..6e14392a33 --- /dev/null +++ b/tests/unit/geometry/test_get_triangle_orientation.py @@ -0,0 +1,12 @@ +from arcade.geometry import get_triangle_orientation + + +def test_get_triangle_orientation(): + triangle_colinear = [(0, 0), (0, 50), (0, 100)] + assert get_triangle_orientation(*triangle_colinear) == 0 + + triangle_cw = [(0, 0), (0, 50), (50, 50)] + assert get_triangle_orientation(*triangle_cw) == 1 + + triangle_ccw = list(reversed(triangle_cw)) + assert get_triangle_orientation(*triangle_ccw) == 2 diff --git a/tests/unit/geometry/test_is_point_in_box.py b/tests/unit/geometry/test_is_point_in_box.py new file mode 100644 index 0000000000..811f907037 --- /dev/null +++ b/tests/unit/geometry/test_is_point_in_box.py @@ -0,0 +1,21 @@ +from arcade.geometry import is_point_in_box + +BOX_START = (0, 0) +BOX_END = (100, 100) + + +def test_point_inside(): + # Point inside box + assert is_point_in_box((0, 0), (50, 50), (100, 100)) is True + assert is_point_in_box((0, 0), (-50, -50), (-100, -100)) is True + assert is_point_in_box((0, 0), (0, 0), (100, 100)) is True + + +def test_point_outside(): + # Point outside box + assert is_point_in_box((0, 0), (-1, -1), (100, 100)) is False + assert is_point_in_box((0, 0), (101, 101), (100, 100)) is False + + +def test_point_intersecting(): + pass diff --git a/tests/unit/geometry/test_is_point_in_polygon.py b/tests/unit/geometry/test_is_point_in_polygon.py new file mode 100644 index 0000000000..046814dafc --- /dev/null +++ b/tests/unit/geometry/test_is_point_in_polygon.py @@ -0,0 +1,37 @@ +from arcade.geometry import is_point_in_polygon + + +def test_point_in_rectangle(): + polygon = [ + (0, 0), + (0, 50), + (50, 50), + (50, 0), + ] + result = is_point_in_polygon(25, 25, polygon) + assert result is True + + +def test_point_not_in_rectangle(): + polygon = [ + (0, 0), + (0, 50), + (50, 50), + (50, 0), + ] + result = is_point_in_polygon(100, 100, polygon) + assert result is False + + +def test_point_not_in_empty_polygon(): + polygon = [] + result = is_point_in_polygon(25, 25, polygon) + assert result is False + + +def test_point_in_extreme_polygon(): + # Cf : https://github.com/pythonarcade/arcade/issues/1906 + polygon = [(9984.0, 2112.0), (10048.0, 2112.0), (10048.0, 2048.0), (9984.0, 2048.0)] + + assert is_point_in_polygon(10016.0, 2080.0, polygon) + diff --git a/tests/unit/test_geometry.py b/tests/unit/test_geometry.py deleted file mode 100644 index fbbf855d18..0000000000 --- a/tests/unit/test_geometry.py +++ /dev/null @@ -1,118 +0,0 @@ -from arcade.geometry import ( - is_point_in_polygon, - are_polygons_intersecting, - get_triangle_orientation, - are_lines_intersecting, - is_point_in_box, -) - - -def test_point_in_rectangle(): - polygon = [ - (0, 0), - (0, 50), - (50, 50), - (50, 0), - ] - result = is_point_in_polygon(25, 25, polygon) - assert result is True - - -def test_point_not_in_rectangle(): - polygon = [ - (0, 0), - (0, 50), - (50, 50), - (50, 0), - ] - result = is_point_in_polygon(100, 100, polygon) - assert result is False - - -def test_point_not_in_empty_polygon(): - polygon = [] - result = is_point_in_polygon(25, 25, polygon) - assert result is False - - -def test_point_in_extreme_polygon(): - # Cf : https://github.com/pythonarcade/arcade/issues/1906 - polygon = [(9984.0, 2112.0), (10048.0, 2112.0), (10048.0, 2048.0), (9984.0, 2048.0)] - - assert is_point_in_polygon(10016.0, 2080.0, polygon) - - -def test_are_polygons_intersecting(): - poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] - poly_b = [(25, 25), (25, 75), (75, 75), (75, 25)] - assert are_polygons_intersecting(poly_a, poly_b) is True - - -def test_are_empty_polygons_breaking(): - poly_a = [] - poly_b = [] - assert are_polygons_intersecting(poly_a, poly_b) is False - - -def test_are_mismatched_polygons_breaking(): - poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] - poly_b = [] - assert are_polygons_intersecting(poly_a, poly_b) is False - - -def test_get_triangle_orientation(): - triangle_colinear = [(0, 0), (0, 50), (0, 100)] - assert get_triangle_orientation(*triangle_colinear) == 0 - - triangle_cw = [(0, 0), (0, 50), (50, 50)] - assert get_triangle_orientation(*triangle_cw) == 1 - - triangle_ccw = list(reversed(triangle_cw)) - assert get_triangle_orientation(*triangle_ccw) == 2 - - -def test_are_lines_intersecting(): - line_a = [(0, 0), (50, 50)] - line_b = [(0, 0), (50, 50)] - assert are_lines_intersecting(*line_a, *line_b) is True - - # --------- - # Two lines clearly intersecting - line_a = [(0, 0), (50, 50)] - line_b = [(0, 50), (50, 0)] - assert are_lines_intersecting(*line_a, *line_b) is True - - # Two parallel lines clearly not intersecting - line_a = [(0, 0), (50, 0)] - line_b = [(0, 50), (0, 50)] - assert are_lines_intersecting(*line_a, *line_b) is False - - # Two lines intersecting at the edge points - line_a = [(0, 0), (50, 0)] - line_b = [(0, -50), (0, 50)] - assert are_lines_intersecting(*line_a, *line_b) is True - - # Twp perpendicular lines almost intersecting - line_a = [(0, 0), (50, 0)] - line_b = [(-1, -50), (-1, 50)] - assert are_lines_intersecting(*line_a, *line_b) is False - - # Twp perpendicular lines almost intersecting - line_a = [(0, 0), (50, 0)] - line_b = [(51, -50), (51, 50)] - assert are_lines_intersecting(*line_a, *line_b) is False - - -def test_is_point_in_box(): - # Point inside box - assert is_point_in_box((0, 0), (50, 50), (100, 100)) is True - assert is_point_in_box((0, 0), (-50, -50), (-100, -100)) is True - assert is_point_in_box((0, 0), (0, 0), (100, 100)) is True - - # Point outside box - assert is_point_in_box((0, 0), (-1, -1), (100, 100)) is False - assert is_point_in_box((0, 0), (101, 101), (100, 100)) is False - - -if __name__ == "__main__": - test_are_lines_intersecting() From 6b96ce1f6154e0f941187d2ad7945f07b3d92fc1 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 14 Jul 2024 15:00:13 +0200 Subject: [PATCH 2/8] split up geometry tests --- .../geometry/test_are_lines_intersecting.py | 33 +++++ .../test_are_polygons_intersecting.py | 19 +++ .../geometry/test_get_triangle_orientation.py | 12 ++ tests/unit/geometry/test_is_point_in_box.py | 21 ++++ .../unit/geometry/test_is_point_in_polygon.py | 37 ++++++ tests/unit/test_geometry.py | 118 ------------------ 6 files changed, 122 insertions(+), 118 deletions(-) create mode 100644 tests/unit/geometry/test_are_lines_intersecting.py create mode 100644 tests/unit/geometry/test_are_polygons_intersecting.py create mode 100644 tests/unit/geometry/test_get_triangle_orientation.py create mode 100644 tests/unit/geometry/test_is_point_in_box.py create mode 100644 tests/unit/geometry/test_is_point_in_polygon.py delete mode 100644 tests/unit/test_geometry.py diff --git a/tests/unit/geometry/test_are_lines_intersecting.py b/tests/unit/geometry/test_are_lines_intersecting.py new file mode 100644 index 0000000000..f35ac1941d --- /dev/null +++ b/tests/unit/geometry/test_are_lines_intersecting.py @@ -0,0 +1,33 @@ +from arcade.geometry import are_lines_intersecting + + +def test_are_lines_intersecting(): + line_a = [(0, 0), (50, 50)] + line_b = [(0, 0), (50, 50)] + assert are_lines_intersecting(*line_a, *line_b) is True + + # --------- + # Two lines clearly intersecting + line_a = [(0, 0), (50, 50)] + line_b = [(0, 50), (50, 0)] + assert are_lines_intersecting(*line_a, *line_b) is True + + # Two parallel lines clearly not intersecting + line_a = [(0, 0), (50, 0)] + line_b = [(0, 50), (0, 50)] + assert are_lines_intersecting(*line_a, *line_b) is False + + # Two lines intersecting at the edge points + line_a = [(0, 0), (50, 0)] + line_b = [(0, -50), (0, 50)] + assert are_lines_intersecting(*line_a, *line_b) is True + + # Twp perpendicular lines almost intersecting + line_a = [(0, 0), (50, 0)] + line_b = [(-1, -50), (-1, 50)] + assert are_lines_intersecting(*line_a, *line_b) is False + + # Twp perpendicular lines almost intersecting + line_a = [(0, 0), (50, 0)] + line_b = [(51, -50), (51, 50)] + assert are_lines_intersecting(*line_a, *line_b) is False diff --git a/tests/unit/geometry/test_are_polygons_intersecting.py b/tests/unit/geometry/test_are_polygons_intersecting.py new file mode 100644 index 0000000000..163c506990 --- /dev/null +++ b/tests/unit/geometry/test_are_polygons_intersecting.py @@ -0,0 +1,19 @@ +from arcade.geometry import are_polygons_intersecting + + +def test_are_polygons_intersecting(): + poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] + poly_b = [(25, 25), (25, 75), (75, 75), (75, 25)] + assert are_polygons_intersecting(poly_a, poly_b) is True + + +def test_are_empty_polygons_breaking(): + poly_a = [] + poly_b = [] + assert are_polygons_intersecting(poly_a, poly_b) is False + + +def test_are_mismatched_polygons_breaking(): + poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] + poly_b = [] + assert are_polygons_intersecting(poly_a, poly_b) is False diff --git a/tests/unit/geometry/test_get_triangle_orientation.py b/tests/unit/geometry/test_get_triangle_orientation.py new file mode 100644 index 0000000000..6e14392a33 --- /dev/null +++ b/tests/unit/geometry/test_get_triangle_orientation.py @@ -0,0 +1,12 @@ +from arcade.geometry import get_triangle_orientation + + +def test_get_triangle_orientation(): + triangle_colinear = [(0, 0), (0, 50), (0, 100)] + assert get_triangle_orientation(*triangle_colinear) == 0 + + triangle_cw = [(0, 0), (0, 50), (50, 50)] + assert get_triangle_orientation(*triangle_cw) == 1 + + triangle_ccw = list(reversed(triangle_cw)) + assert get_triangle_orientation(*triangle_ccw) == 2 diff --git a/tests/unit/geometry/test_is_point_in_box.py b/tests/unit/geometry/test_is_point_in_box.py new file mode 100644 index 0000000000..811f907037 --- /dev/null +++ b/tests/unit/geometry/test_is_point_in_box.py @@ -0,0 +1,21 @@ +from arcade.geometry import is_point_in_box + +BOX_START = (0, 0) +BOX_END = (100, 100) + + +def test_point_inside(): + # Point inside box + assert is_point_in_box((0, 0), (50, 50), (100, 100)) is True + assert is_point_in_box((0, 0), (-50, -50), (-100, -100)) is True + assert is_point_in_box((0, 0), (0, 0), (100, 100)) is True + + +def test_point_outside(): + # Point outside box + assert is_point_in_box((0, 0), (-1, -1), (100, 100)) is False + assert is_point_in_box((0, 0), (101, 101), (100, 100)) is False + + +def test_point_intersecting(): + pass diff --git a/tests/unit/geometry/test_is_point_in_polygon.py b/tests/unit/geometry/test_is_point_in_polygon.py new file mode 100644 index 0000000000..046814dafc --- /dev/null +++ b/tests/unit/geometry/test_is_point_in_polygon.py @@ -0,0 +1,37 @@ +from arcade.geometry import is_point_in_polygon + + +def test_point_in_rectangle(): + polygon = [ + (0, 0), + (0, 50), + (50, 50), + (50, 0), + ] + result = is_point_in_polygon(25, 25, polygon) + assert result is True + + +def test_point_not_in_rectangle(): + polygon = [ + (0, 0), + (0, 50), + (50, 50), + (50, 0), + ] + result = is_point_in_polygon(100, 100, polygon) + assert result is False + + +def test_point_not_in_empty_polygon(): + polygon = [] + result = is_point_in_polygon(25, 25, polygon) + assert result is False + + +def test_point_in_extreme_polygon(): + # Cf : https://github.com/pythonarcade/arcade/issues/1906 + polygon = [(9984.0, 2112.0), (10048.0, 2112.0), (10048.0, 2048.0), (9984.0, 2048.0)] + + assert is_point_in_polygon(10016.0, 2080.0, polygon) + diff --git a/tests/unit/test_geometry.py b/tests/unit/test_geometry.py deleted file mode 100644 index fbbf855d18..0000000000 --- a/tests/unit/test_geometry.py +++ /dev/null @@ -1,118 +0,0 @@ -from arcade.geometry import ( - is_point_in_polygon, - are_polygons_intersecting, - get_triangle_orientation, - are_lines_intersecting, - is_point_in_box, -) - - -def test_point_in_rectangle(): - polygon = [ - (0, 0), - (0, 50), - (50, 50), - (50, 0), - ] - result = is_point_in_polygon(25, 25, polygon) - assert result is True - - -def test_point_not_in_rectangle(): - polygon = [ - (0, 0), - (0, 50), - (50, 50), - (50, 0), - ] - result = is_point_in_polygon(100, 100, polygon) - assert result is False - - -def test_point_not_in_empty_polygon(): - polygon = [] - result = is_point_in_polygon(25, 25, polygon) - assert result is False - - -def test_point_in_extreme_polygon(): - # Cf : https://github.com/pythonarcade/arcade/issues/1906 - polygon = [(9984.0, 2112.0), (10048.0, 2112.0), (10048.0, 2048.0), (9984.0, 2048.0)] - - assert is_point_in_polygon(10016.0, 2080.0, polygon) - - -def test_are_polygons_intersecting(): - poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] - poly_b = [(25, 25), (25, 75), (75, 75), (75, 25)] - assert are_polygons_intersecting(poly_a, poly_b) is True - - -def test_are_empty_polygons_breaking(): - poly_a = [] - poly_b = [] - assert are_polygons_intersecting(poly_a, poly_b) is False - - -def test_are_mismatched_polygons_breaking(): - poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] - poly_b = [] - assert are_polygons_intersecting(poly_a, poly_b) is False - - -def test_get_triangle_orientation(): - triangle_colinear = [(0, 0), (0, 50), (0, 100)] - assert get_triangle_orientation(*triangle_colinear) == 0 - - triangle_cw = [(0, 0), (0, 50), (50, 50)] - assert get_triangle_orientation(*triangle_cw) == 1 - - triangle_ccw = list(reversed(triangle_cw)) - assert get_triangle_orientation(*triangle_ccw) == 2 - - -def test_are_lines_intersecting(): - line_a = [(0, 0), (50, 50)] - line_b = [(0, 0), (50, 50)] - assert are_lines_intersecting(*line_a, *line_b) is True - - # --------- - # Two lines clearly intersecting - line_a = [(0, 0), (50, 50)] - line_b = [(0, 50), (50, 0)] - assert are_lines_intersecting(*line_a, *line_b) is True - - # Two parallel lines clearly not intersecting - line_a = [(0, 0), (50, 0)] - line_b = [(0, 50), (0, 50)] - assert are_lines_intersecting(*line_a, *line_b) is False - - # Two lines intersecting at the edge points - line_a = [(0, 0), (50, 0)] - line_b = [(0, -50), (0, 50)] - assert are_lines_intersecting(*line_a, *line_b) is True - - # Twp perpendicular lines almost intersecting - line_a = [(0, 0), (50, 0)] - line_b = [(-1, -50), (-1, 50)] - assert are_lines_intersecting(*line_a, *line_b) is False - - # Twp perpendicular lines almost intersecting - line_a = [(0, 0), (50, 0)] - line_b = [(51, -50), (51, 50)] - assert are_lines_intersecting(*line_a, *line_b) is False - - -def test_is_point_in_box(): - # Point inside box - assert is_point_in_box((0, 0), (50, 50), (100, 100)) is True - assert is_point_in_box((0, 0), (-50, -50), (-100, -100)) is True - assert is_point_in_box((0, 0), (0, 0), (100, 100)) is True - - # Point outside box - assert is_point_in_box((0, 0), (-1, -1), (100, 100)) is False - assert is_point_in_box((0, 0), (101, 101), (100, 100)) is False - - -if __name__ == "__main__": - test_are_lines_intersecting() From bf557699af8d81ac3e804f894373475c3352b90b Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Jul 2024 01:57:48 -0400 Subject: [PATCH 3/8] Experimental smoke test tool based on einarf's prototype --- .../sprite_collision_inspector.py | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 tests/manual_smoke/sprite_collision_inspector.py diff --git a/tests/manual_smoke/sprite_collision_inspector.py b/tests/manual_smoke/sprite_collision_inspector.py new file mode 100644 index 0000000000..b18ac88f2f --- /dev/null +++ b/tests/manual_smoke/sprite_collision_inspector.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +import builtins +from typing import TypeVar, Type, Generic, Any, Callable + +from pyglet.math import Vec2 + +import arcade +from arcade import SpriteList, Sprite, SpriteSolidColor, load_texture +from arcade.gui import UIManager, NinePatchTexture, UIInputText, UIWidget, UIBoxLayout +from arcade.types import RGBOrA255, Color + +GRID_REGULAR = arcade.color.GREEN.replace(a=128) +GRID_HIGHLIGHT = arcade.color.GREEN + + +TEX_GREY_PANEL_RAW = load_texture(":resources:gui_basic_assets/window/grey_panel.png") + +T = TypeVar('T') + +def _tname(t: Any) -> str: + if not isinstance(t, builtins.type): + return t.__class__.__name__ + else: + return t.__name__ + + +class TypedTextInput(UIInputText, Generic[T]): + def __init__( + self, + parsed_type: Type[T], + *, + to_str: Callable[[T], str] = repr, + from_str: Callable[[str], T] | None = None, + x: float = 0, + y: float = 0, + width: float = 100, + height: float = 24, + text: str = "", + font_name=("Arial",), + font_size: float = 12, + text_color: RGBOrA255 = (0, 0, 0, 255), + error_color: RGBOrA255 = arcade.color.RED, + multiline=False, + size_hint=None, + size_hint_min=None, + size_hint_max=None, + **kwargs, + ): + if not isinstance(type, builtins.type): + raise TypeError(f"Expected a type, but got {type}") + super().__init__( + x=x, + y=y, + width=width, + height=height, + text=text, + font_name=font_name, + font_size=font_size, + text_color=text_color, + multiline=multiline, + caret_color=text_color, + size_hint=size_hint, + size_hint_min=size_hint_min, + size_hint_max=size_hint_max, + **kwargs + ) + self._error_color = error_color + self._parsed_type: Type[T] = parsed_type + self._to_str = to_str + self._from_str = from_str or parsed_type + self._parsed_value: T = self._from_str(self.text) + + @property + def value(self) -> T: + return self._parsed_value + + @value.setter + def value(self, new_value: T) -> None: + if not isinstance(new_value, self._parsed_type): + raise TypeError( + f"This {_tname(self)} is of inner type {_tname(self._parsed_type)}" + f", but got {new_value!r}, a {_tname(new_value)}" + ) + try: + self._parsed_value = self._from_str(new_value) + self.doc.text = self._to_str(new_value) + self.color = self._text_color + except Exception as e: + self.color = self._error_color + raise e + + self.trigger_full_render() + + @property + def color(self) -> Color: + return self._color + + @color.setter + def color(self, new_color: RGBOrA255) -> None: + # lol, efficiency + validated = Color.from_iterable(new_color) + if self._color == validated: + return + + self.caret.color = validated + self.doc.set_style( + 0, len(self.text), dict(color=validated) + ) + self.trigger_full_render() + + @property + def text(self) -> str: + return self.doc.text + + @text.setter + def text(self, new_text: str) -> None: + try: + self.doc.text = new_text + validated: T = self._from_str(new_text) + self._parsed_value = validated + self.color = self._text_color + except Exception as e: + self.color = self._error_color + raise e + + + +def draw_crosshair( + where: tuple[float, float], + color=arcade.color.BLACK, + radius: float = 20.0, + border_width: float = 1.0, +) -> None: + x, y = where + arcade.draw.circle.draw_circle_outline( + x, y, + radius, + color=color, + border_width=border_width + ) + arcade.draw.draw_line( + x, y - radius, x, y + radius, + color=color, line_width=border_width) + + arcade.draw.draw_line( + x - radius, y, x + radius, y, + color=color, line_width=border_width) + + +class MyGame(arcade.Window): + + def add_field_row(self, label_text: str, widget: UIWidget) -> None: + children = ( + arcade.gui.widgets.text.UITextArea( + text=label_text, + width=100, + height=20, + color=arcade.color.BLACK, + font_size=12 + ), + widget + ) + row = UIBoxLayout(vertical=False, space_between=10, children=children) + self.rows.add(row) + + def __init__( + self, + width: int = 1280, + height: int = 720, + grid_tile_px: int = 100 + ): + + super().__init__(width, height, "Collision Inspector") + # why does this need a context again? + self.nine_patch = NinePatchTexture( + left=5, right=5, top=5, bottom=5, texture=TEX_GREY_PANEL_RAW) + self.ui = UIManager() + self.spritelist: SpriteList[Sprite] = arcade.SpriteList() + + + textbox_template = dict(width=40, height=20, text_color=arcade.color.BLACK) + self.cursor_x_field = UIInputText( + text="1.0", **textbox_template).with_background(texture=self.nine_patch) + + self.cursor_y_field = UIInputText( + text="1.0", **textbox_template).with_background(texture=self.nine_patch) + + self.rows = UIBoxLayout(space_between=20).with_background(color=arcade.color.GRAY) + + self.grid_tile_px = grid_tile_px + self.ui.add(self.rows) + + self.add_field_row("Cursor Y", self.cursor_y_field) + self.add_field_row("Cursor X", self.cursor_x_field) + self.ui.enable() + + # for y in range(8): + # for x in range(12): + # sprite = SpriteSolidColor(grid_tile_px, grid_tile_px, color=arcade.color.WHITE) + # sprite.position = x * 101 + 50, y * 101 + 50 + # self.spritelist.append(sprite) + self.build_sprite_grid(8, 12, self.grid_tile_px, Vec2(50, 50)) + self.background_color = arcade.color.DARK_GRAY + self.set_mouse_visible(False) + self.cursor = 0, 0 + self.from_mouse = True + self.on_widget = False + + def build_sprite_grid( + self, + columns: int, + rows: int, + grid_tile_px: int, + offset: tuple[float, float] = (0, 0) + ): + offset_x, offset_y = offset + self.spritelist.clear() + + for row in range(rows): + x = offset_x + grid_tile_px * row + for column in range(columns): + y = offset_y + grid_tile_px * column + sprite = SpriteSolidColor(grid_tile_px, grid_tile_px, color=arcade.color.WHITE) + sprite.position = x, y + self.spritelist.append(sprite) + + def on_update(self, dt: float = 1 / 60): + self.cursor = Vec2(self.mouse["x"], self.mouse["y"]) + + widgets = list(self.ui.get_widgets_at(self.cursor)) + on_widget = bool(len(widgets)) + + if self.on_widget != on_widget: + self.set_mouse_visible(on_widget) + self.on_widget = on_widget + + def on_draw(self): + self.clear() + # Reset color + for sprite in self.spritelist: + sprite.color = arcade.color.WHITE + # sprite.angle += 0.2 + + # Mark hits + hits = arcade.get_sprites_at_point(self.cursor, self.spritelist) + for hit in hits: + hit.color = arcade.color.BLUE + + self.spritelist.draw() + self.spritelist.draw_hit_boxes(color=arcade.color.GREEN) + if hits: + arcade.draw.rect.draw_rect_outline(rect=hits[0].rect, color=arcade.color.RED) + if not self.on_widget: + draw_crosshair(self.cursor) + + self.ui.draw() + +MyGame().run() \ No newline at end of file From 29c3f708ab211ef9bea9280df4da664ae067511c Mon Sep 17 00:00:00 2001 From: "Wilson (Fengchi) Wang" Date: Wed, 7 Aug 2024 17:24:26 -0400 Subject: [PATCH 4/8] Point in polygon fix (#2336) * Fixed get point in poly when getting a horizontal edge * legacy changes --- arcade/geometry.py | 10 ++++++++++ tests/unit/sprite/test_sprite_collision.py | 16 ++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/arcade/geometry.py b/arcade/geometry.py index 0f3b6deb87..50de80c82c 100644 --- a/arcade/geometry.py +++ b/arcade/geometry.py @@ -191,6 +191,16 @@ def is_point_in_polygon(x: float, y: float, polygon: Point2List) -> bool: if polygon[i][1] == p[1]: decrease += 1 + # Check if the point is exactly on a horizontal edge + if polygon[i][1] == y and polygon[next_item][1] == y: + # Check if the point is on the line segment + if (polygon[i][0] <= x <= polygon[next_item][0]) or ( + polygon[next_item][0] <= x <= polygon[i][0] + ): + return True + else: + return False + # Check if the line segment from 'p' to # 'extreme' intersects with the line # segment from 'polygon[i]' to 'polygon[next]' diff --git a/tests/unit/sprite/test_sprite_collision.py b/tests/unit/sprite/test_sprite_collision.py index 942c0ebbeb..85f3e75c49 100644 --- a/tests/unit/sprite/test_sprite_collision.py +++ b/tests/unit/sprite/test_sprite_collision.py @@ -7,16 +7,22 @@ def test_sprites_at_point(): coin_list = arcade.SpriteList() sprite = arcade.SpriteSolidColor(50, 50, color=arcade.csscolor.RED) + # an adjacent sprite with the same level horizontal bottom edge + sprite2 = arcade.SpriteSolidColor(50, 50, center_x=50, center_y=0, color=arcade.csscolor.RED) + coin_list.append(sprite) + coin_list.append(sprite2) - # print() - # print(sprite.points) sprite_list = arcade.get_sprites_at_point((0, 0), coin_list) assert len(sprite_list) == 1 + sprite_list = arcade.get_sprites_at_point((0, 50), coin_list) + assert len(sprite_list) == 1 + + sprite_list = arcade.get_sprites_at_point((0, -25), coin_list) + assert len(sprite_list) == 1 + sprite.position = (130, 130) - # print() - # print(sprite.points) sprite_list = arcade.get_sprites_at_point((0, 0), coin_list) assert len(sprite_list) == 0 @@ -25,8 +31,6 @@ def test_sprites_at_point(): assert len(sprite_list) == 1 sprite.angle = 90 - # print() - # print(sprite.points) sprite_list = arcade.get_sprites_at_point((0, 0), coin_list) assert len(sprite_list) == 0 From 1814a59cfe2f953cb3a971b2971ee704bde388be Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 28 Sep 2024 15:35:31 +0200 Subject: [PATCH 5/8] Test cleanup --- tests/unit/atlas/test_region.py | 2 +- .../geometry/test_are_lines_intersecting.py | 3 +-- .../test_are_polygons_intersecting.py | 9 +++++-- tests/unit/geometry/test_is_point_in_box.py | 27 +++++++++++-------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/unit/atlas/test_region.py b/tests/unit/atlas/test_region.py index 6b99121921..59dbdfdb0f 100644 --- a/tests/unit/atlas/test_region.py +++ b/tests/unit/atlas/test_region.py @@ -3,7 +3,7 @@ import PIL.Image from arcade.texture_atlas.region import AtlasRegion -from arcade.texture.texture import Texture, ImageData +from arcade.texture.texture import ImageData from arcade.texture_atlas.atlas_default import DefaultTextureAtlas diff --git a/tests/unit/geometry/test_are_lines_intersecting.py b/tests/unit/geometry/test_are_lines_intersecting.py index f35ac1941d..782ed648c8 100644 --- a/tests/unit/geometry/test_are_lines_intersecting.py +++ b/tests/unit/geometry/test_are_lines_intersecting.py @@ -6,7 +6,6 @@ def test_are_lines_intersecting(): line_b = [(0, 0), (50, 50)] assert are_lines_intersecting(*line_a, *line_b) is True - # --------- # Two lines clearly intersecting line_a = [(0, 0), (50, 50)] line_b = [(0, 50), (50, 0)] @@ -22,7 +21,7 @@ def test_are_lines_intersecting(): line_b = [(0, -50), (0, 50)] assert are_lines_intersecting(*line_a, *line_b) is True - # Twp perpendicular lines almost intersecting + # Two perpendicular lines almost intersecting line_a = [(0, 0), (50, 0)] line_b = [(-1, -50), (-1, 50)] assert are_lines_intersecting(*line_a, *line_b) is False diff --git a/tests/unit/geometry/test_are_polygons_intersecting.py b/tests/unit/geometry/test_are_polygons_intersecting.py index 163c506990..54b744379b 100644 --- a/tests/unit/geometry/test_are_polygons_intersecting.py +++ b/tests/unit/geometry/test_are_polygons_intersecting.py @@ -1,19 +1,24 @@ from arcade.geometry import are_polygons_intersecting -def test_are_polygons_intersecting(): +def test_intersecting_clear_case(): + """Two polygons clearly intersecting""" poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] poly_b = [(25, 25), (25, 75), (75, 75), (75, 25)] assert are_polygons_intersecting(poly_a, poly_b) is True + assert are_polygons_intersecting(poly_b, poly_a) is True -def test_are_empty_polygons_breaking(): +def test_empty_polygons(): + """Two empty polys should never intersect""" poly_a = [] poly_b = [] assert are_polygons_intersecting(poly_a, poly_b) is False def test_are_mismatched_polygons_breaking(): + """One empty poly should never intersect with a non-empty poly""" poly_a = [(0, 0), (0, 50), (50, 50), (50, 0)] poly_b = [] assert are_polygons_intersecting(poly_a, poly_b) is False + assert are_polygons_intersecting(poly_b, poly_a) is False diff --git a/tests/unit/geometry/test_is_point_in_box.py b/tests/unit/geometry/test_is_point_in_box.py index 811f907037..0639e3179d 100644 --- a/tests/unit/geometry/test_is_point_in_box.py +++ b/tests/unit/geometry/test_is_point_in_box.py @@ -1,21 +1,26 @@ from arcade.geometry import is_point_in_box -BOX_START = (0, 0) -BOX_END = (100, 100) - -def test_point_inside(): - # Point inside box +def test_point_inside_center(): + """Points clearly inside the box""" assert is_point_in_box((0, 0), (50, 50), (100, 100)) is True assert is_point_in_box((0, 0), (-50, -50), (-100, -100)) is True + assert is_point_in_box((0, 0), (50, -50), (100, -100)) is True + assert is_point_in_box((0, 0), (-50, 50), (-100, 100)) is True + + +def test_point_intersecting(): + """Points intersecting the box edges""" + # Test each corner assert is_point_in_box((0, 0), (0, 0), (100, 100)) is True + assert is_point_in_box((0, 0), (100, 100), (100, 100)) is True + assert is_point_in_box((0, 0), (100, 0), (100, 100)) is True + assert is_point_in_box((0, 0), (0, 100), (100, 100)) is True -def test_point_outside(): - # Point outside box +def test_point_outside_1px(): + """Points outside the box by one pixel""" assert is_point_in_box((0, 0), (-1, -1), (100, 100)) is False assert is_point_in_box((0, 0), (101, 101), (100, 100)) is False - - -def test_point_intersecting(): - pass + assert is_point_in_box((0, 0), (101, -1), (100, 100)) is False + assert is_point_in_box((0, 0), (-1, 101), (100, 100)) is False From c39c1efc8b8c9a6edec9013176d6d4ecc691ac72 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 28 Sep 2024 18:12:13 +0200 Subject: [PATCH 6/8] tests: Point in box with zero width or height --- tests/unit/geometry/test_is_point_in_box.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/geometry/test_is_point_in_box.py b/tests/unit/geometry/test_is_point_in_box.py index 0639e3179d..8cbd408d22 100644 --- a/tests/unit/geometry/test_is_point_in_box.py +++ b/tests/unit/geometry/test_is_point_in_box.py @@ -24,3 +24,17 @@ def test_point_outside_1px(): assert is_point_in_box((0, 0), (101, 101), (100, 100)) is False assert is_point_in_box((0, 0), (101, -1), (100, 100)) is False assert is_point_in_box((0, 0), (-1, 101), (100, 100)) is False + + +def test_zero_box(): + """ + A box selection with zero width or height + + The selection area should always be included as a hit. + """ + # 1 x 1 pixel box + assert is_point_in_box((0, 0), (0, 0), (0, 0)) is True + # 1 x 100 pixel box + assert is_point_in_box((0, 0), (50, 0), (100, 0)) is True + # 100 x 1 pixel box + assert is_point_in_box((0, 0), (0, 50), (0, 100)) is True From fa1dadb92d676c4519f02d3c561d62556244f65e Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 28 Sep 2024 19:38:30 +0200 Subject: [PATCH 7/8] Fix collision code and tests --- arcade/geometry.py | 12 +------ .../unit/geometry/test_is_point_in_polygon.py | 31 +++++++++---------- tests/unit/sprite/test_sprite_collision.py | 14 ++++----- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/arcade/geometry.py b/arcade/geometry.py index 50de80c82c..739d1c3955 100644 --- a/arcade/geometry.py +++ b/arcade/geometry.py @@ -191,16 +191,6 @@ def is_point_in_polygon(x: float, y: float, polygon: Point2List) -> bool: if polygon[i][1] == p[1]: decrease += 1 - # Check if the point is exactly on a horizontal edge - if polygon[i][1] == y and polygon[next_item][1] == y: - # Check if the point is on the line segment - if (polygon[i][0] <= x <= polygon[next_item][0]) or ( - polygon[next_item][0] <= x <= polygon[i][0] - ): - return True - else: - return False - # Check if the line segment from 'p' to # 'extreme' intersects with the line # segment from 'polygon[i]' to 'polygon[next]' @@ -209,7 +199,7 @@ def is_point_in_polygon(x: float, y: float, polygon: Point2List) -> bool: # segment 'i-next', then check if it lies # on segment. If it lies, return true, otherwise false if get_triangle_orientation(polygon[i], p, polygon[next_item]) == 0: - return not is_point_in_box( + return is_point_in_box( polygon[i], p, polygon[next_item], diff --git a/tests/unit/geometry/test_is_point_in_polygon.py b/tests/unit/geometry/test_is_point_in_polygon.py index 046814dafc..5bee6b7073 100644 --- a/tests/unit/geometry/test_is_point_in_polygon.py +++ b/tests/unit/geometry/test_is_point_in_polygon.py @@ -2,23 +2,24 @@ def test_point_in_rectangle(): - polygon = [ - (0, 0), - (0, 50), - (50, 50), - (50, 0), - ] - result = is_point_in_polygon(25, 25, polygon) - assert result is True + polygon = [(0, 0), (0, 50), (50, 50), (50, 0)] + # Center + assert is_point_in_polygon(25, 25, polygon) is True + # One pixel from edge + assert is_point_in_polygon(1, 1, polygon) is True + assert is_point_in_polygon(49, 49, polygon) is True + assert is_point_in_polygon(1, 49, polygon) is True + assert is_point_in_polygon(49, 1, polygon) is True + + # Intersect edges + assert is_point_in_polygon(0, 0, polygon) is True + assert is_point_in_polygon(50, 50, polygon) is True + assert is_point_in_polygon(0, 50, polygon) is True + assert is_point_in_polygon(50, 0, polygon) is True def test_point_not_in_rectangle(): - polygon = [ - (0, 0), - (0, 50), - (50, 50), - (50, 0), - ] + polygon = [(0, 0), (0, 50), (50, 50), (50, 0)] result = is_point_in_polygon(100, 100, polygon) assert result is False @@ -32,6 +33,4 @@ def test_point_not_in_empty_polygon(): def test_point_in_extreme_polygon(): # Cf : https://github.com/pythonarcade/arcade/issues/1906 polygon = [(9984.0, 2112.0), (10048.0, 2112.0), (10048.0, 2048.0), (9984.0, 2048.0)] - assert is_point_in_polygon(10016.0, 2080.0, polygon) - diff --git a/tests/unit/sprite/test_sprite_collision.py b/tests/unit/sprite/test_sprite_collision.py index 85f3e75c49..0de3e2e6a6 100644 --- a/tests/unit/sprite/test_sprite_collision.py +++ b/tests/unit/sprite/test_sprite_collision.py @@ -16,7 +16,7 @@ def test_sprites_at_point(): sprite_list = arcade.get_sprites_at_point((0, 0), coin_list) assert len(sprite_list) == 1 - sprite_list = arcade.get_sprites_at_point((0, 50), coin_list) + sprite_list = arcade.get_sprites_at_point((50, 0), coin_list) assert len(sprite_list) == 1 sprite_list = arcade.get_sprites_at_point((0, -25), coin_list) @@ -55,17 +55,15 @@ def test_sprite_collides_with_point(): assert sprite.collides_with_point(point) is True # Negative - point = (0, 1) + point = (0, 2) assert sprite.collides_with_point(point) is False - point = (1, 0) + point = (2, 0) assert sprite.collides_with_point(point) is False - point = (1, 1) - assert sprite.collides_with_point(point) is False - point = (-1, -1) + point = (2, 2) assert sprite.collides_with_point(point) is False - point = (-1, 0) + point = (-2, -2) assert sprite.collides_with_point(point) is False - point = (2, 2) + point = (-2, 0) assert sprite.collides_with_point(point) is False From bc2d7e80722baed8e38540d8b6003e2bb7d7f729 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 28 Sep 2024 19:43:13 +0200 Subject: [PATCH 8/8] Include both cc and cw polygons --- .../unit/geometry/test_is_point_in_polygon.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unit/geometry/test_is_point_in_polygon.py b/tests/unit/geometry/test_is_point_in_polygon.py index 5bee6b7073..20b650ac2d 100644 --- a/tests/unit/geometry/test_is_point_in_polygon.py +++ b/tests/unit/geometry/test_is_point_in_polygon.py @@ -1,7 +1,8 @@ from arcade.geometry import is_point_in_polygon -def test_point_in_rectangle(): +def test_point_in_rectangle_cw(): + """Clockwise rectangle""" polygon = [(0, 0), (0, 50), (50, 50), (50, 0)] # Center assert is_point_in_polygon(25, 25, polygon) is True @@ -18,6 +19,24 @@ def test_point_in_rectangle(): assert is_point_in_polygon(50, 0, polygon) is True +def test_point_in_rectangle_cc(): + """Counter-clockwise rectangle""" + polygon = [(0, 0), (50, 0), (50, 50), (0, 50)] + # Center + assert is_point_in_polygon(25, 25, polygon) is True + # One pixel from edge + assert is_point_in_polygon(1, 1, polygon) is True + assert is_point_in_polygon(49, 49, polygon) is True + assert is_point_in_polygon(1, 49, polygon) is True + assert is_point_in_polygon(49, 1, polygon) is True + + # Intersect edges + assert is_point_in_polygon(0, 0, polygon) is True + assert is_point_in_polygon(50, 50, polygon) is True + assert is_point_in_polygon(0, 50, polygon) is True + assert is_point_in_polygon(50, 0, polygon) is True + + def test_point_not_in_rectangle(): polygon = [(0, 0), (0, 50), (50, 50), (50, 0)] result = is_point_in_polygon(100, 100, polygon)