Files
rigid-body/collider/types.py
2026-03-13 18:33:18 -04:00

141 lines
4.4 KiB
Python

from dataclasses import dataclass
from typing import Generator
from abc import ABC, abstractmethod
from math import pi, cos, sin
import pygame as pg
from transform import Transform
@dataclass
class Face:
begin: pg.Vector2
end: pg.Vector2
@property
def normal(self) -> pg.Vector2:
return (self.end-self.begin).rotate(-90).normalize()
@dataclass
class ColliderContact:
__slots__ = ["points", "normal", "penetration"]
points: list[pg.Vector2]
normal: pg.Vector2
penetration: float
@dataclass(frozen=True)
class PolygonalHull:
_vertices: list[pg.Vector2]
def get_vertex_at_index(self, key: int) -> pg.Vector2:
return self._vertices[key]
def get_face_at_index(self, key: int) -> Face:
return Face(self._vertices[key], self._vertices[(key + 1) % len(self._vertices)])
def vertices(self) -> Generator[pg.Vector2, None, None]:
for v in self._vertices:
yield v
def faces(self) -> Generator[Face, None, None]:
for i in range(len(self._vertices)):
yield self.get_face_at_index(i)
def project(self, axis: pg.Vector2) -> tuple[float, float]:
projections = [v.dot(axis) for v in self._vertices]
return (min(projections), max(projections))
def closest_vertex(self, v: pg.Vector2) -> pg.Vector2:
return min(self._vertices, key=lambda p: (v-p).length())
class BaseCollider(ABC):
@abstractmethod
def moment_of_inertia(self, mass: float) -> float:
pass
class ConvexCollider(BaseCollider):
@abstractmethod
def hull(self, transform: Transform) -> PolygonalHull:
pass
@dataclass
class CircleCollider(BaseCollider):
radius: float
def moment_of_inertia(self, mass: float) -> float:
return 0.5 * self.radius ** 2 * mass
@dataclass
class RectCollider(ConvexCollider):
dimensions: tuple[float, float]
@property
def width(self):
return self.dimensions[0]
@property
def height(self):
return self.dimensions[1]
def hull(self, transform: Transform) -> PolygonalHull:
return PolygonalHull([
transform.global_position - pg.Vector2(self.width / 2.0, self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale,
transform.global_position - pg.Vector2(-self.width / 2.0, self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale,
transform.global_position - pg.Vector2(-self.width / 2.0, -self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale,
transform.global_position - pg.Vector2(self.width / 2.0, -self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale
])
def moment_of_inertia(self, mass: float) -> float:
return (1.0 / 12.0) * mass * (self.width ** 2 + self.height ** 2)
@dataclass
class CapsuleCollider(ConvexCollider):
width: float
height: float
resolution: int = 8
@property
def radius(self) -> float:
return self.width / 2
@property
def _half_height(self) -> float:
return (self.height / 2) - self.radius
@dataclass
class CapsuleCollider(ConvexCollider):
width: float
height: float
resolution: int = 10
@property
def radius(self) -> float:
return self.width / 2
def hull(self, transform: Transform) -> PolygonalHull:
verts: list[pg.Vector2] = []
center_offset = (self.height / 2.0 - self.radius)
top_center = transform.global_position - pg.Vector2(0, center_offset).rotate(transform.global_degrees) * transform.global_scale
bottom_center = transform.global_position + pg.Vector2(0, center_offset).rotate(transform.global_degrees) * transform.global_scale
angle = pi
for _ in range(0, self.resolution):
verts.append(
top_center + (self.radius * pg.Vector2(cos(angle), -sin(angle))).rotate(transform.global_degrees) * transform.global_scale
)
angle -= pi / self.resolution
angle = 0.0
for _ in range(0, self.resolution):
verts.append(
bottom_center + (self.radius * pg.Vector2(cos(angle), -sin(angle))).rotate(transform.global_degrees) * transform.global_scale
)
angle -= pi / self.resolution
return PolygonalHull(verts)
def moment_of_inertia(self, mass: float) -> float:
return (1.0 / 12.0) * mass * (self.width ** 2 + self.height ** 2)