140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
import pygame
|
|
from ..geom import Polygon, generate_polygon
|
|
from .. import WIDTH, HEIGHT, get_cursor_pos, get_inputs
|
|
from ..colors import *
|
|
from ..math import *
|
|
from ..draw import *
|
|
|
|
|
|
def compute_separating_axes(p1, p2):
|
|
"""Compute the projections upon the separating axes of polygons p1 and p2.
|
|
|
|
For efficient collision detection, this could early-exit as soon as one
|
|
axis is found where the projections of p1 and p2 do not overlap. For
|
|
visualization purposes, we compute the projections for all axes and
|
|
return the results."""
|
|
|
|
# The list of axes to project upon
|
|
axes = []
|
|
|
|
# Iterate the normals of both polygons
|
|
for normal in p1.get_normals() + p2.get_normals():
|
|
# we can trim out normals that are in the same or opposite direction
|
|
# as axes we already have, since they will give us the same overlap
|
|
# information
|
|
if not any(is_colinear(*normal, *axis) for axis in axes):
|
|
axes.append(normal)
|
|
|
|
# helper function to find the min/max projections of vertices onto
|
|
# the given axis.
|
|
|
|
def get_min_max(axis, vertices):
|
|
projections = [dot(*axis, *vertex) for vertex in vertices]
|
|
return min(projections), max(projections)
|
|
|
|
return [
|
|
(axis,
|
|
get_min_max(axis, p1.get_translated_vertices()),
|
|
get_min_max(axis, p2.get_translated_vertices()))
|
|
for axis in axes
|
|
]
|
|
|
|
|
|
class SeparatingAxisTheorem:
|
|
|
|
title = "Separating Axis Theorem"
|
|
|
|
def __init__(self):
|
|
self.shape1 = generate_polygon(0, 0, sides=4, radius=60)
|
|
|
|
self.shape2 = None
|
|
self.shape2_sides = 4
|
|
self.shape2_radius = 60
|
|
self.generate_shape2()
|
|
|
|
def generate_shape2(self):
|
|
"""Generates the shape we can move around - a regular polygon."""
|
|
rotation = 45 if self.shape2 is None else self.shape2.rotation
|
|
self.shape2 = generate_polygon(*get_cursor_pos(),
|
|
rotation,
|
|
sides=self.shape2_sides,
|
|
radius=self.shape2_radius)
|
|
|
|
def handle_key_down(self, key):
|
|
if key == pygame.K_a:
|
|
# remove a side from the polygon
|
|
self.shape2_sides = max(self.shape2_sides - 1, 3)
|
|
self.generate_shape2()
|
|
if key == pygame.K_d:
|
|
# add a side to the polygon
|
|
self.shape2_sides = min(self.shape2_sides + 1, 31)
|
|
self.generate_shape2()
|
|
|
|
def handle_input(self):
|
|
self.shape2.set_position(*get_cursor_pos())
|
|
|
|
inputs = get_inputs()
|
|
if inputs[pygame.K_q]:
|
|
# rotate counter-clockwise by 90 degrees every second
|
|
self.shape2.rotate((1/60) * -90)
|
|
if inputs[pygame.K_e]:
|
|
# same as above, but clockwise
|
|
self.shape2.rotate((1/60) * 90)
|
|
|
|
def render(self, surface):
|
|
self.handle_input()
|
|
|
|
gaps = []
|
|
|
|
for result in compute_separating_axes(self.shape1, self.shape2):
|
|
|
|
axis, (min1, max1), (min2, max2) = result
|
|
|
|
# length of the gap between the two projections
|
|
gap = max(min2 - max1, min1 - max2)
|
|
gaps.append(gap)
|
|
|
|
self.draw_axis(surface, axis, gap, min1, max1, min2, max2)
|
|
|
|
colliding = all(gap < 0 for gap in gaps)
|
|
if colliding:
|
|
text_screen(surface, WHITE, (10, HEIGHT - 20),
|
|
f"Min Distance to Resolve: {max(gaps)}")
|
|
|
|
self.shape1.draw(surface, YELLOW, 1)
|
|
self.shape2.draw(surface, RED if colliding else WHITE, 1)
|
|
|
|
def draw_axis(self, surface, normal, gap, min1, max1, min2, max2):
|
|
"""Draws a separating axis with shape projections, and separating line.
|
|
Uses colours to clearly indicate whether the projections overlap."""
|
|
|
|
# since the normal's origin is 0,0 (center of the screen), calcuate
|
|
# an offset to move all drawing off to one side - in this case 300px.
|
|
offset = scale(*rnormal(*normal), 300)
|
|
|
|
overlapping = gap < 0
|
|
|
|
if not overlapping:
|
|
# how far from the center of the gap is from the origin
|
|
dist = scale(*normal, (min1 if min1 > min2 else min2) - 0.5 * gap)
|
|
|
|
# the separating line
|
|
line(surface, CYAN,
|
|
add(*dist, *offset),
|
|
add(*dist, *scale(*lnormal(*normal), 1000)))
|
|
|
|
# axis, extending off the screen in both directions
|
|
line(surface, WHITE,
|
|
add(*offset, *scale(*reverse(*normal), 1000)),
|
|
add(*offset, *scale(*normal, 1000)))
|
|
|
|
# projection 1
|
|
line(surface, RED if overlapping else GREEN,
|
|
add(*offset, *scale(*normal, min1)),
|
|
add(*offset, *scale(*normal, max1)), 8)
|
|
|
|
# projection 2
|
|
line(surface, RED if overlapping else GREEN,
|
|
add(*offset, *scale(*normal, min2)),
|
|
add(*offset, *scale(*normal, max2)), 8)
|