"""
This module is used for calculating hit boxes
"""
import pymunk
from PIL import Image
from arcade import NamedPoint
from arcade import Point
from typing import List, Union, Tuple
from pymunk import autogeometry
[docs]def calculate_hit_box_points_simple(image: Image) -> List[Point]:
"""
Given an image, this returns points that make up a hit box around it. Attempts
to trim out transparent pixels.
:param Image image:
:Returns: List of points
"""
left_border = 0
good = True
while good and left_border < image.width:
for row in range(image.height):
pos = (left_border, row)
pixel = image.getpixel(pos)
if type(pixel) is int or len(pixel) != 4:
raise TypeError("Error, calculate_points called on image not in RGBA format")
else:
if pixel[3] != 0:
good = False
break
if good:
left_border += 1
right_border = image.width - 1
good = True
while good and right_border > 0:
for row in range(image.height):
pos = (right_border, row)
pixel = image.getpixel(pos)
if pixel[3] != 0:
good = False
break
if good:
right_border -= 1
top_border = 0
good = True
while good and top_border < image.height:
for column in range(image.width):
pos = (column, top_border)
pixel = image.getpixel(pos)
if pixel[3] != 0:
good = False
break
if good:
top_border += 1
bottom_border = image.height - 1
good = True
while good and bottom_border > 0:
for column in range(image.width):
pos = (column, bottom_border)
pixel = image.getpixel(pos)
if pixel[3] != 0:
good = False
break
if good:
bottom_border -= 1
# If the image is empty, return an empty set
if bottom_border == 0:
return []
def _check_corner_offset(start_x: int, start_y: int, x_direction: int, y_direction: int) -> int:
bad = False
offset = 0
while not bad:
y = start_y + (offset * y_direction)
x = start_x
for count in range(offset + 1):
my_pixel = image.getpixel((x, y))
# print(f"({x}, {y}) = {pixel} | ", end="")
if my_pixel[3] != 0:
bad = True
break
y -= y_direction
x += x_direction
# print(f" - {bad}")
if not bad:
offset += 1
# print(f"offset: {offset}")
return offset
def _r(point: Tuple[float, float], height: int, width: int):
return point[0] - width / 2, (height - point[1]) - height / 2
top_left_corner_offset = _check_corner_offset(left_border, top_border, 1, 1)
top_right_corner_offset = _check_corner_offset(right_border, top_border, -1, 1)
bottom_left_corner_offset = _check_corner_offset(left_border, bottom_border, 1, -1)
bottom_right_corner_offset = _check_corner_offset(right_border, bottom_border, -1, -1)
p1 = left_border + top_left_corner_offset, top_border
p2 = (right_border + 1) - top_right_corner_offset, top_border
p3 = (right_border + 1), top_border + top_right_corner_offset
p4 = (right_border + 1), (bottom_border + 1) - bottom_right_corner_offset
p5 = (right_border + 1) - bottom_right_corner_offset, (bottom_border + 1)
p6 = left_border + bottom_left_corner_offset, (bottom_border + 1)
p7 = left_border, (bottom_border + 1) - bottom_left_corner_offset
p8 = left_border, top_border + top_left_corner_offset
result = []
h = image.height
w = image.width
result.append(_r(p7, h, w))
if bottom_left_corner_offset:
result.append(_r(p6, h, w))
result.append(_r(p5, h, w))
if bottom_right_corner_offset:
result.append(_r(p4, h, w))
result.append(_r(p3, h, w))
if top_right_corner_offset:
result.append(_r(p2, h, w))
result.append(_r(p1, h, w))
if top_left_corner_offset:
result.append(_r(p8, h, w))
# Remove duplicates
result = tuple(dict.fromkeys(result))
return result
[docs]def calculate_hit_box_points_detailed(image: Image,
hit_box_detail: float = 4.5)\
-> Union[List[NamedPoint], Tuple[NamedPoint, ...]]:
"""
Given an image, this returns points that make up a hit box around it. Attempts
to trim out transparent pixels.
:param Image image: Image get hit box from.
:param int hit_box_detail: How detailed to make the hit box. There's a
trade-off in number of points vs. accuracy.
:Returns: List of points
"""
def sample_func(sample_point: Point) -> int:
""" Method used to sample image. """
if sample_point[0] < 0 \
or sample_point[1] < 0 \
or sample_point[0] >= image.width \
or sample_point[1] >= image.height:
return 0
point_tuple = sample_point[0], sample_point[1]
color = image.getpixel(point_tuple)
if color[3] > 0:
return 255
else:
return 0
# Do a quick check if it is a full tile
p1 = 0, 0
p2 = 0, image.height - 1
p3 = image.width - 1, image.height - 1
p4 = image.width - 1, 0
if sample_func(p1) and sample_func(p2) and sample_func(p3) and sample_func(p4):
# Do a quick check if it is a full tile
p1 = (-image.width / 2, -image.height / 2)
p2 = (image.width / 2, -image.height / 2)
p3 = (image.width / 2, image.height / 2)
p4 = (-image.width / 2, image.height / 2)
return p1, p2, p3, p4
# Get the bounding box
logo_bb = pymunk.BB(-1, -1, image.width, image.height)
# Set of lines that trace the image
line_set = pymunk.autogeometry.PolylineSet()
# How often to sample?
downres = 1
horizontal_samples = int(image.width / downres)
vertical_samples = int(image.height / downres)
# Run the trace
# Get back one or more sets of lines covering stuff.
line_sets = pymunk.autogeometry.march_soft(
logo_bb,
horizontal_samples, vertical_samples,
99,
sample_func)
if len(line_sets) == 0:
return []
selected_line_set = line_sets[0]
selected_range = None
if len(line_set) > 1:
# We have more than one line set. Try and find one that covers most of
# the sprite.
for line in line_set:
min_x = None
min_y = None
max_x = None
max_y = None
for point in line:
if min_x is None or point.x < min_x:
min_x = point.x
if max_x is None or point.x > max_x:
max_x = point.x
if min_y is None or point.y < min_y:
min_y = point.y
if max_y is None or point.y > max_y:
max_y = point.y
if min_x is None or max_x is None or min_y is None or max_y is None:
raise ValueError("No points in bounding box.")
my_range = max_x - min_x + max_y + min_y
if selected_range is None or my_range > selected_range:
selected_range = my_range
selected_line_set = line
# Reduce number of vertices
# original_points = len(selected_line_set)
selected_line_set = pymunk.autogeometry.simplify_curves(selected_line_set,
hit_box_detail)
# downsampled_points = len(selected_line_set)
# Convert to normal points, offset fo 0,0 is center, flip the y
hh = image.height / 2
hw = image.width / 2
points = []
for vec2 in selected_line_set:
point = round(vec2.x - hw), round(image.height - (vec2.y - hh) - image.height)
points.append(point)
if len(points) > 1 and points[0] == points[-1]:
points.pop()
# print(f"{sprite.texture.name} Line-sets={len(line_set)}, Original points={original_points}, Downsampled points={downsampled_points}")
return points