feat(layout_optimizer): add segment_obb_intersection_length (Cyrus-Beck clipping)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yexiaozhou
2026-04-02 13:24:45 +08:00
parent 31e79e9aff
commit 569ac4a931
2 changed files with 90 additions and 1 deletions

View File

@@ -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]],

View File

@@ -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