Created
February 21, 2026 13:36
-
-
Save EncodeTheCode/1f0fca03ffeef4fb063dc92223c8b426 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import numpy as np | |
| from typing import Callable, Optional | |
| # ------------------------- | |
| # Utility functions | |
| # ------------------------- | |
| def aabb_overlap(min_a: np.ndarray, max_a: np.ndarray, min_b: np.ndarray, max_b: np.ndarray) -> bool: | |
| """Fast AABB overlap test: True if boxes overlap on X,Y,Z.""" | |
| # vectorized: overlap if max_a >= min_b and max_b >= min_a on all axes | |
| return np.all(max_a >= min_b) and np.all(max_b >= min_a) | |
| def sphere_overlap(center_a: np.ndarray, r_a: float, center_b: np.ndarray, r_b: float) -> bool: | |
| """Bounding-sphere overlap test.""" | |
| # compare squared distances to avoid sqrt | |
| d2 = np.sum((center_a - center_b) ** 2) | |
| rr = (r_a + r_b) ** 2 | |
| return d2 <= rr | |
| # ------------------------- | |
| # Core classes | |
| # ------------------------- | |
| class CollisionBehavior: | |
| """Container for callbacks and trigger control.""" | |
| __slots__ = ("callback", "trigger_once", "triggered") | |
| def __init__(self, callback: Optional[Callable] = None, trigger_once: bool = True): | |
| self.callback = callback | |
| self.trigger_once = trigger_once | |
| self.triggered = False | |
| def try_trigger(self, *args, **kwargs): | |
| """Call callback if allowed by trigger_once state.""" | |
| if self.callback is None: | |
| return | |
| if not self.trigger_once or not self.triggered: | |
| # call user callback | |
| self.callback(*args, **kwargs) | |
| self.triggered = True | |
| def reset(self): | |
| self.triggered = False | |
| class Entity: | |
| """ | |
| Minimal memory footprint entity. | |
| - position: numpy array shape (3,) | |
| - aabb_min/aabb_max: numpy arrays shape (3,) cached for fast tests | |
| - bs_center/bs_radius: bounding sphere optional | |
| - vertices: optional numpy array (n,3) if you need vertex-level construction; can be freed. | |
| """ | |
| __slots__ = ( | |
| "position", "aabb_min", "aabb_max", | |
| "bs_center", "bs_radius", | |
| "vertices", "collision_behavior", "_dirty" | |
| ) | |
| def __init__( | |
| self, | |
| position=(0.0, 0.0, 0.0), | |
| half_extents=(0.5, 0.5, 0.5), | |
| *, | |
| vertices: Optional[np.ndarray] = None, | |
| callback: Optional[Callable] = None, | |
| trigger_once: bool = True | |
| ): | |
| # keep position as float64 numpy vector | |
| self.position = np.asarray(position, dtype=np.float64) | |
| self.vertices = None if vertices is None else np.asarray(vertices, dtype=np.float64) | |
| self.collision_behavior = CollisionBehavior(callback=callback, trigger_once=trigger_once) | |
| # If vertices provided, build bounds from them (local space assumed) | |
| if self.vertices is not None: | |
| self._build_bounds_from_vertices() | |
| else: | |
| # center = position, half extents define AABB around center | |
| he = np.asarray(half_extents, dtype=np.float64) | |
| self.aabb_min = self.position - he | |
| self.aabb_max = self.position + he | |
| self.bs_center = self.position.copy() | |
| self.bs_radius = float(np.linalg.norm(he)) | |
| self._dirty = False # if you later change position or vertices set dirty=True | |
| # --- bounds construction --- | |
| def _build_bounds_from_vertices(self): | |
| """Compute AABB and bounding sphere from vertex array (assumes vertices in world coords).""" | |
| v = self.vertices | |
| # use vectorized numpy min/max | |
| vmin = v.min(axis=0) | |
| vmax = v.max(axis=0) | |
| self.aabb_min = vmin.copy() | |
| self.aabb_max = vmax.copy() | |
| # sphere center = midpoint of aabb, radius = max distance to corner | |
| center = (vmin + vmax) / 2.0 | |
| self.bs_center = center | |
| # compute radius squared then sqrt | |
| self.bs_radius = float(np.sqrt(np.max(np.sum((v - center) ** 2, axis=1)))) | |
| # make sure position is consistent with center (optional) | |
| self.position = center.copy() | |
| def recompute_bounds_if_dirty(self): | |
| """Call this if you mutate position or vertices and set _dirty = True.""" | |
| if not getattr(self, "_dirty", False): | |
| return | |
| if self.vertices is not None: | |
| self._build_bounds_from_vertices() | |
| else: | |
| # nothing to do without vertices unless user changed position externally | |
| pass | |
| self._dirty = False | |
| def free_vertices(self): | |
| """Free vertex buffer to save memory after bounds are built.""" | |
| self.vertices = None | |
| # --- convenience --- | |
| def set_position(self, new_pos): | |
| self.position = np.asarray(new_pos, dtype=np.float64) | |
| # translate AABB and sphere by delta if needed | |
| # assume aabb currently centered at previous self.position; compute delta: | |
| center = (self.aabb_min + self.aabb_max) / 2.0 | |
| delta = self.position - center | |
| self.aabb_min = self.aabb_min + delta | |
| self.aabb_max = self.aabb_max + delta | |
| self.bs_center = self.bs_center + delta | |
| def reset_triggered(self): | |
| self.collision_behavior.reset() | |
| # ------------------------- | |
| # Single-call collision check & trigger | |
| # ------------------------- | |
| def check_collision(a: Entity, b: Entity, method: str = "aabb_and_sphere") -> bool: | |
| """ | |
| Efficient collision check between two entities. | |
| - method options: | |
| - 'aabb' : only AABB vs AABB | |
| - 'sphere' : bounding-sphere only (fast, conservative) | |
| - 'aabb_and_sphere' (default): cheap sphere test first, then exact AABB test | |
| Returns True if overlap. | |
| """ | |
| # ensure cached bounds are up-to-date (cheap check) | |
| a.recompute_bounds_if_dirty() | |
| b.recompute_bounds_if_dirty() | |
| if method == "sphere": | |
| return sphere_overlap(a.bs_center, a.bs_radius, b.bs_center, b.bs_radius) | |
| if method == "aabb": | |
| return aabb_overlap(a.aabb_min, a.aabb_max, b.aabb_min, b.aabb_max) | |
| # default: cheap sphere reject, then AABB check | |
| if not sphere_overlap(a.bs_center, a.bs_radius, b.bs_center, b.bs_radius): | |
| return False | |
| return aabb_overlap(a.aabb_min, a.aabb_max, b.aabb_min, b.aabb_max) | |
| def check_and_trigger(a: Entity, b: Entity, method: str = "aabb_and_sphere", call_both: bool = True) -> bool: | |
| """ | |
| Check collision and trigger callbacks: | |
| - If a collision occurs: | |
| - runs a.collision_behavior.try_trigger(a, b) | |
| - if call_both True, also runs b.collision_behavior.try_trigger(b, a) | |
| - Returns True if collision detected. | |
| """ | |
| collided = check_collision(a, b, method=method) | |
| if collided: | |
| a.collision_behavior.try_trigger(a, b) | |
| if call_both: | |
| b.collision_behavior.try_trigger(b, a) | |
| else: | |
| # If not collided and the user wants one-shot triggers to be re-armed behaviorally | |
| # we do NOT automatically reset triggered flags here; leaving control to user is nicer. | |
| # If you want automatic rearming when they separate: uncomment next two lines. | |
| # a.reset_triggered() | |
| # b.reset_triggered() | |
| pass | |
| return collided |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment