190 lines
6.3 KiB
Python
190 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import List, Optional
|
|
|
|
from Box2D import (b2AABB, b2Body, b2CircleShape, b2PolygonShape, b2QueryCallback,
|
|
b2RayCastCallback, b2Transform, b2Vec2, b2TestOverlap)
|
|
|
|
|
|
@dataclass
|
|
class RayHit:
|
|
"""Raycast hit data.
|
|
|
|
Attributes:
|
|
hit: True if the ray hit something.
|
|
body: The body hit, if any.
|
|
fraction: Fraction along the ray where the hit occurred.
|
|
distance: Distance along the ray in world units.
|
|
point: World-space hit point.
|
|
normal: World-space hit normal.
|
|
"""
|
|
hit: bool = False
|
|
body: Optional[b2Body] = None
|
|
fraction: float = 1.0
|
|
distance: float = 0.0
|
|
point: b2Vec2 = b2Vec2(0.0, 0.0)
|
|
normal: b2Vec2 = b2Vec2(0.0, 0.0)
|
|
|
|
|
|
class _RayCastClosest(b2RayCastCallback):
|
|
def __init__(self, ignore_body: Optional[b2Body], translation: b2Vec2, result: RayHit) -> None:
|
|
super().__init__()
|
|
self.ignore_body = ignore_body
|
|
self.translation = translation
|
|
self.result = result
|
|
|
|
def ReportFixture(self, fixture, point, normal, fraction): # noqa: N802
|
|
if self.ignore_body is not None and fixture.body == self.ignore_body:
|
|
return 1.0
|
|
if fraction < self.result.fraction:
|
|
self.result.hit = True
|
|
self.result.fraction = fraction
|
|
self.result.distance = self.translation.length * fraction
|
|
self.result.point = point
|
|
self.result.normal = normal
|
|
self.result.body = fixture.body
|
|
return fraction
|
|
|
|
|
|
def raycast_closest(world, ignore_body: Optional[b2Body], origin: b2Vec2, translation: b2Vec2) -> RayHit:
|
|
"""Cast a ray and return the closest hit.
|
|
|
|
Args:
|
|
world: Box2D world to query.
|
|
ignore_body: Optional body to ignore.
|
|
origin: Ray start in world units.
|
|
translation: Ray delta in world units.
|
|
|
|
Returns:
|
|
RayHit data for the closest hit (or empty hit if none).
|
|
"""
|
|
result = RayHit()
|
|
callback = _RayCastClosest(ignore_body, translation, result)
|
|
world.RayCast(callback, origin, origin + translation)
|
|
return result
|
|
|
|
|
|
def raycast_all(world, ignore_body: Optional[b2Body], origin: b2Vec2, translation: b2Vec2) -> List[RayHit]:
|
|
"""Cast a ray and return all hits.
|
|
|
|
Args:
|
|
world: Box2D world to query.
|
|
ignore_body: Optional body to ignore.
|
|
origin: Ray start in world units.
|
|
translation: Ray delta in world units.
|
|
|
|
Returns:
|
|
List of RayHit results.
|
|
"""
|
|
hits: List[RayHit] = []
|
|
|
|
class _RayCastAll(b2RayCastCallback):
|
|
def __init__(self, ignore: Optional[b2Body], translation_vec: b2Vec2) -> None:
|
|
super().__init__()
|
|
self.ignore = ignore
|
|
self.translation_vec = translation_vec
|
|
|
|
def ReportFixture(self, fixture, point, normal, fraction): # noqa: N802
|
|
if self.ignore is not None and fixture.body == self.ignore:
|
|
return 1.0
|
|
hit = RayHit(True, fixture.body, fraction, self.translation_vec.length * fraction, point, normal)
|
|
hits.append(hit)
|
|
return fraction
|
|
|
|
world.RayCast(_RayCastAll(ignore_body, translation), origin, origin + translation)
|
|
return hits
|
|
|
|
|
|
def _aabb_for_circle(center: b2Vec2, radius: float) -> b2AABB:
|
|
lower = b2Vec2(center.x - radius, center.y - radius)
|
|
upper = b2Vec2(center.x + radius, center.y + radius)
|
|
return b2AABB(lowerBound=lower, upperBound=upper)
|
|
|
|
|
|
def _aabb_for_box(center: b2Vec2, half_w: float, half_h: float, angle: float) -> b2AABB:
|
|
# Conservative AABB for rotated box
|
|
import math
|
|
|
|
cos_a = abs(math.cos(angle))
|
|
sin_a = abs(math.sin(angle))
|
|
extent_x = half_w * cos_a + half_h * sin_a
|
|
extent_y = half_w * sin_a + half_h * cos_a
|
|
lower = b2Vec2(center.x - extent_x, center.y - extent_y)
|
|
upper = b2Vec2(center.x + extent_x, center.y + extent_y)
|
|
return b2AABB(lowerBound=lower, upperBound=upper)
|
|
|
|
|
|
def shape_hit(world, ignore_body: Optional[b2Body], shape, transform: b2Transform) -> List[b2Body]:
|
|
"""Query overlaps for a shape and return hit bodies.
|
|
|
|
Args:
|
|
world: Box2D world to query.
|
|
ignore_body: Optional body to ignore.
|
|
shape: Box2D shape to test.
|
|
transform: Transform for the shape.
|
|
|
|
Returns:
|
|
List of bodies overlapping the shape.
|
|
"""
|
|
aabb = shape.getAABB(transform, 0)
|
|
hits: List[b2Body] = []
|
|
|
|
class _QueryCallback(b2QueryCallback):
|
|
def __init__(self, ignore: Optional[b2Body]) -> None:
|
|
super().__init__()
|
|
self.ignore = ignore
|
|
|
|
def ReportFixture(self, fixture): # noqa: N802
|
|
body = fixture.body
|
|
if self.ignore is not None and body == self.ignore:
|
|
return True
|
|
if b2TestOverlap(shape, 0, fixture.shape, 0, transform, fixture.body.transform):
|
|
if body not in hits:
|
|
hits.append(body)
|
|
return True
|
|
|
|
world.QueryAABB(_QueryCallback(ignore_body), aabb)
|
|
return hits
|
|
|
|
|
|
def circle_hit(world, ignore_body: Optional[b2Body], center: b2Vec2, radius: float) -> List[b2Body]:
|
|
"""Check for circle overlaps in the world.
|
|
|
|
Args:
|
|
world: Box2D world to query.
|
|
ignore_body: Optional body to ignore.
|
|
center: Circle center in world units.
|
|
radius: Circle radius in world units.
|
|
|
|
Returns:
|
|
List of bodies overlapping the circle.
|
|
"""
|
|
shape = b2CircleShape(radius=radius, pos=b2Vec2(0.0, 0.0))
|
|
transform = b2Transform()
|
|
transform.position = center
|
|
transform.angle = 0.0
|
|
return shape_hit(world, ignore_body, shape, transform)
|
|
|
|
|
|
def rectangle_hit(world, ignore_body: Optional[b2Body], center: b2Vec2, size: b2Vec2, rotation: float = 0.0) -> List[b2Body]:
|
|
"""Check for rectangle overlaps in the world.
|
|
|
|
Args:
|
|
world: Box2D world to query.
|
|
ignore_body: Optional body to ignore.
|
|
center: Rectangle center in world units.
|
|
size: Rectangle size in world units.
|
|
rotation: Rotation in radians.
|
|
|
|
Returns:
|
|
List of bodies overlapping the rectangle.
|
|
"""
|
|
half_w = size.x / 2.0
|
|
half_h = size.y / 2.0
|
|
shape = b2PolygonShape(box=(half_w, half_h))
|
|
transform = b2Transform()
|
|
transform.position = center
|
|
transform.angle = rotation
|
|
return shape_hit(world, ignore_body, shape, transform)
|