"""OBB (Oriented Bounding Box) geometry: corners, overlap (SAT), minimum distance.""" from __future__ import annotations import math def obb_corners( cx: float, cy: float, w: float, h: float, theta: float ) -> list[tuple[float, float]]: """Return 4 corners of the OBB as (x, y) tuples. Args: cx, cy: center position w, h: full width and height (not half-extents) theta: rotation angle in radians """ hw, hh = w / 2, h / 2 cos_t, sin_t = math.cos(theta), math.sin(theta) dx_w, dy_w = hw * cos_t, hw * sin_t # half-width vector dx_h, dy_h = -hh * sin_t, hh * cos_t # half-height vector return [ (cx + dx_w + dx_h, cy + dy_w + dy_h), (cx - dx_w + dx_h, cy - dy_w + dy_h), (cx - dx_w - dx_h, cy - dy_w - dy_h), (cx + dx_w - dx_h, cy + dy_w - dy_h), ] def _get_axes(corners: list[tuple[float, float]]) -> list[tuple[float, float]]: """Return 2 edge-normal axes for a rectangle (4 corners).""" axes = [] for i in range(2): # Only need 2 axes for a rectangle edge_x = corners[i + 1][0] - corners[i][0] edge_y = corners[i + 1][1] - corners[i][1] length = math.sqrt(edge_x**2 + edge_y**2) if length > 0: axes.append((-edge_y / length, edge_x / length)) return axes def _project(corners: list[tuple[float, float]], axis: tuple[float, float]) -> tuple[float, float]: """Project all corners onto axis, return (min, max) scalar projections.""" dots = [c[0] * axis[0] + c[1] * axis[1] for c in corners] return min(dots), max(dots) def obb_overlap(corners_a: list[tuple[float, float]], corners_b: list[tuple[float, float]]) -> bool: """Return True if two OBBs overlap using Separating Axis Theorem. Uses strict inequality (touching edges = no overlap). """ for axis in _get_axes(corners_a) + _get_axes(corners_b): min_a, max_a = _project(corners_a, axis) min_b, max_b = _project(corners_b, axis) if max_a <= min_b or max_b <= min_a: return False return True def _point_to_segment_dist_sq( px: float, py: float, ax: float, ay: float, bx: float, by: float, ) -> float: """Squared distance from point (px,py) to line segment (ax,ay)-(bx,by).""" dx, dy = bx - ax, by - ay len_sq = dx * dx + dy * dy if len_sq == 0: return (px - ax) ** 2 + (py - ay) ** 2 t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / len_sq)) proj_x, proj_y = ax + t * dx, ay + t * dy return (px - proj_x) ** 2 + (py - proj_y) ** 2 def obb_penetration_depth( corners_a: list[tuple[float, float]], corners_b: list[tuple[float, float]], ) -> float: """Minimum penetration depth between two OBBs (SAT-based). Returns 0.0 if not overlapping. Otherwise returns the minimum overlap along any separating axis — the smallest push needed to separate them. """ min_overlap = float("inf") for axis in _get_axes(corners_a) + _get_axes(corners_b): min_a, max_a = _project(corners_a, axis) min_b, max_b = _project(corners_b, axis) overlap = min(max_a - min_b, max_b - min_a) if overlap <= 0: return 0.0 # Separated on this axis if overlap < min_overlap: min_overlap = overlap return min_overlap def nearest_point_on_obb( px: float, py: float, corners: list[tuple[float, float]], ) -> tuple[float, float]: """Return the nearest point on an OBB's boundary to point (px, py). If the point is inside the OBB, returns the nearest edge point. """ best_x, best_y = corners[0] best_dist_sq = float("inf") n = len(corners) for i in range(n): ax, ay = corners[i] bx, by = corners[(i + 1) % n] dx, dy = bx - ax, by - ay len_sq = dx * dx + dy * dy if len_sq == 0: proj_x, proj_y = ax, ay else: t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / len_sq)) proj_x, proj_y = ax + t * dx, ay + t * dy d_sq = (px - proj_x) ** 2 + (py - proj_y) ** 2 if d_sq < best_dist_sq: best_dist_sq = d_sq best_x, best_y = proj_x, proj_y return best_x, best_y def segment_intersects_obb( p1: tuple[float, float], p2: tuple[float, float], corners: list[tuple[float, float]], ) -> bool: """Return True if line segment p1-p2 intersects the OBB (convex polygon). Uses separating axis theorem on the Minkowski difference: test segment against each edge of the polygon + segment normal. """ # Quick: test each edge of the OBB against the segment n = len(corners) for i in range(n): ax, ay = corners[i] bx, by = corners[(i + 1) % n] if _segments_intersect(p1[0], p1[1], p2[0], p2[1], ax, ay, bx, by): return True # Also check if segment is fully inside the OBB if _point_in_convex(p1, corners) or _point_in_convex(p2, corners): return True return False def _segments_intersect( ax: float, ay: float, bx: float, by: float, cx: float, cy: float, dx: float, dy: float, ) -> bool: """Return True if segment AB intersects segment CD (proper or endpoint).""" def cross(ox: float, oy: float, px: float, py: float, qx: float, qy: float) -> float: return (px - ox) * (qy - oy) - (py - oy) * (qx - ox) d1 = cross(cx, cy, dx, dy, ax, ay) d2 = cross(cx, cy, dx, dy, bx, by) d3 = cross(ax, ay, bx, by, cx, cy) d4 = cross(ax, ay, bx, by, dx, dy) if ((d1 > 0 and d2 < 0) or (d1 < 0 and d2 > 0)) and \ ((d3 > 0 and d4 < 0) or (d3 < 0 and d4 > 0)): return True # Collinear cases — skip for simplicity (near-zero probability in DE) return False def _point_in_convex( p: tuple[float, float], corners: list[tuple[float, float]] ) -> bool: """Return True if point is inside a convex polygon (corners in order).""" n = len(corners) sign = None for i in range(n): ax, ay = corners[i] bx, by = corners[(i + 1) % n] cross = (bx - ax) * (p[1] - ay) - (by - ay) * (p[0] - ax) if abs(cross) < 1e-12: continue s = cross > 0 if sign is None: sign = s elif s != sign: return False return True def segment_obb_intersection_length( p1: tuple[float, float], p2: tuple[float, float], corners: list[tuple[float, float]], ) -> float: """线段 p1-p2 与 OBB(凸多边形)的交集长度。 Cyrus-Beck 线段裁剪算法。corners 假定为 CCW 顺序(obb_corners 生成)。 无交集返回 0.0。 """ dx = p2[0] - p1[0] dy = p2[1] - p1[1] seg_len_sq = dx * dx + dy * dy if seg_len_sq < 1e-24: return 0.0 t_enter = 0.0 t_exit = 1.0 n = len(corners) for i in range(n): ax, ay = corners[i] bx, by = corners[(i + 1) % n] # CCW 多边形边的外法线: (ey, -ex), e = b - a ex, ey = bx - ax, by - ay nx, ny = ey, -ex denom = nx * dx + ny * dy numer = nx * (p1[0] - ax) + ny * (p1[1] - ay) if abs(denom) < 1e-12: if numer > 0: return 0.0 # 在此边外侧且平行 continue t = -numer / denom if denom < 0: t_enter = max(t_enter, t) # 进入 else: t_exit = min(t_exit, t) # 退出 if t_enter > t_exit: return 0.0 if t_enter >= t_exit: return 0.0 return (t_exit - t_enter) * math.sqrt(seg_len_sq) def obb_min_distance( corners_a: list[tuple[float, float]], corners_b: list[tuple[float, float]], ) -> float: """Minimum distance between two OBBs (convex polygons). Returns 0.0 if overlapping or touching. """ if obb_overlap(corners_a, corners_b): return 0.0 min_dist_sq = float("inf") for poly, other in [(corners_a, corners_b), (corners_b, corners_a)]: n = len(other) for px, py in poly: for i in range(n): ax, ay = other[i] bx, by = other[(i + 1) % n] d_sq = _point_to_segment_dist_sq(px, py, ax, ay, bx, by) if d_sq < min_dist_sq: min_dist_sq = d_sq return math.sqrt(min_dist_sq)