Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix point in polygon #2347

Merged
merged 11 commits into from
Sep 28, 2024
2 changes: 1 addition & 1 deletion arcade/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,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],
Expand Down
259 changes: 259 additions & 0 deletions tests/manual_smoke/sprite_collision_inspector.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion tests/unit/atlas/test_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
32 changes: 32 additions & 0 deletions tests/unit/geometry/test_are_lines_intersecting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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

# 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

# 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
24 changes: 24 additions & 0 deletions tests/unit/geometry/test_are_polygons_intersecting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from arcade.geometry import 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_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
12 changes: 12 additions & 0 deletions tests/unit/geometry/test_get_triangle_orientation.py
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions tests/unit/geometry/test_is_point_in_box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from arcade.geometry import is_point_in_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_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
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
Loading
Loading