diff --git a/unilabos/layout_optimizer/obb.py b/unilabos/layout_optimizer/obb.py index eaf0a6b2..e89619a1 100644 --- a/unilabos/layout_optimizer/obb.py +++ b/unilabos/layout_optimizer/obb.py @@ -183,6 +183,56 @@ def _point_in_convex( 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]], diff --git a/unilabos/layout_optimizer/tests/test_obb.py b/unilabos/layout_optimizer/tests/test_obb.py index 9fc4d455..c3bbe9e8 100644 --- a/unilabos/layout_optimizer/tests/test_obb.py +++ b/unilabos/layout_optimizer/tests/test_obb.py @@ -1,7 +1,7 @@ """Tests for OBB (Oriented Bounding Box) geometry utilities.""" import math import pytest -from ..obb import obb_corners, obb_overlap, obb_min_distance +from ..obb import obb_corners, obb_overlap, obb_min_distance, segment_obb_intersection_length class TestObbCorners: @@ -115,3 +115,42 @@ class TestObbMinDistance: a = obb_corners(0, 0, 2.0, 2.0, 0.0) b = obb_corners(2.0, 0, 2.0, 2.0, 0.0) assert obb_min_distance(a, b) == pytest.approx(0.0) + + +class TestSegmentOBBIntersectionLength: + """segment_obb_intersection_length: Cyrus-Beck clipping.""" + + def test_segment_fully_outside(self): + corners = obb_corners(0, 0, 2, 2, 0) + length = segment_obb_intersection_length((-5, 3), (5, 3), corners) + assert length == 0.0 + + def test_segment_fully_inside(self): + corners = obb_corners(0, 0, 4, 4, 0) + length = segment_obb_intersection_length((-0.5, 0), (0.5, 0), corners) + assert abs(length - 1.0) < 1e-6 + + def test_segment_crosses_through(self): + corners = obb_corners(0, 0, 2, 2, 0) + length = segment_obb_intersection_length((-5, 0), (5, 0), corners) + assert abs(length - 2.0) < 1e-6 + + def test_segment_partial_overlap(self): + corners = obb_corners(0, 0, 2, 2, 0) + length = segment_obb_intersection_length((0, 0), (5, 0), corners) + assert abs(length - 1.0) < 1e-6 + + def test_rotated_obb(self): + corners = obb_corners(0, 0, 2, 2, math.pi / 4) + length = segment_obb_intersection_length((-3, 0), (3, 0), corners) + expected = 2 * math.sqrt(2) + assert abs(length - expected) < 1e-4 + + def test_zero_length_segment(self): + corners = obb_corners(0, 0, 2, 2, 0) + assert segment_obb_intersection_length((0, 0), (0, 0), corners) == 0.0 + + def test_parallel_outside(self): + corners = obb_corners(0, 0, 2, 2, 0) + length = segment_obb_intersection_length((-5, 2), (5, 2), corners) + assert length == 0.0