"""clash 테스트 (Rhino 없이 실행됩니다). 철근 중심선(선분)끼리의 간섭 검사. 기준: 기존 프로젝트 rebar_pipeline/rhino/08_clash_check.py - 간섭 = rA + rB - (두 선분 최단거리) > 0.5mm - 같은 철근, 같은 박스(겹침이음 모델링) 선분끼리는 제외 - 철근 쌍마다 가장 깊은 관입 하나만 기록 """ import time import pytest import clash as ck def seg(p, q, radius=12.7, bar=0, group=0, mark="C1"): return ck.Seg(p, q, radius, bar, group, mark) # ---- 선분 사이 최단거리 (3D) ---- def test_distance_crossing_skew_segments(): # x축 선분과, z=3 높이에서 y 방향으로 가로지르는 선분 -> 3 d = ck.segment_distance((-5, 0, 0), (5, 0, 0), (0, -5, 3), (0, 5, 3)) assert d == pytest.approx(3.0) def test_distance_parallel_segments(): d = ck.segment_distance((0, 0, 0), (10, 0, 0), (0, 4, 0), (10, 4, 0)) assert d == pytest.approx(4.0) def test_distance_intersecting_segments_is_zero(): d = ck.segment_distance((-5, 0, 0), (5, 0, 0), (0, -5, 0), (0, 5, 0)) assert d == pytest.approx(0.0) def test_distance_collinear_with_gap(): d = ck.segment_distance((0, 0, 0), (1, 0, 0), (3, 0, 0), (4, 0, 0)) assert d == pytest.approx(2.0) def test_distance_closest_at_endpoint(): d = ck.segment_distance((0, 0, 0), (10, 0, 0), (12, 5, 0), (12, 10, 0)) assert d == pytest.approx((2 ** 2 + 5 ** 2) ** 0.5) def test_distance_is_symmetric(): a = ((0, 0, 0), (10, 3, 1)) b = ((4, 8, -2), (9, -6, 5)) assert ck.segment_distance(*(a + b)) == pytest.approx(ck.segment_distance(*(b + a))) def test_distance_point_to_segment(): # 길이 0 선분(점)과 선분 d = ck.segment_distance((5, 3, 0), (5, 3, 0), (0, 0, 0), (10, 0, 0)) assert d == pytest.approx(3.0) def test_distance_two_points(): d = ck.segment_distance((0, 0, 0), (0, 0, 0), (3, 4, 0), (3, 4, 0)) assert d == pytest.approx(5.0) def test_distance_matches_brute_force_on_random_segments(): # 무작위 선분 200쌍을 "촘촘히 점을 찍어 가장 가까운 쌍을 찾는 느린 방법"과 비교 import random rng = random.Random(12345) def pt(): return (rng.uniform(-50, 50), rng.uniform(-50, 50), rng.uniform(-50, 50)) n = 60 for _ in range(200): p1, q1, p2, q2 = pt(), pt(), pt(), pt() best = None for i in range(n + 1): u = i / float(n) a = tuple(p1[k] + (q1[k] - p1[k]) * u for k in range(3)) for j in range(n + 1): v = j / float(n) b = tuple(p2[k] + (q2[k] - p2[k]) * v for k in range(3)) dist = sum((a[k] - b[k]) ** 2 for k in range(3)) ** 0.5 if best is None or dist < best: best = dist fast = ck.segment_distance(p1, q1, p2, q2) # 정확한 값은 점 찍기 값보다 작거나 같고, 격자 간격만큼만 차이가 남 assert fast <= best + 1e-9 assert best - fast < 3.0 # ---- 간섭 판정 ---- def crossing_pair(gap, **kw): """x축 선분과, z = gap 높이에서 y 방향으로 가로지르는 선분.""" a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=1, **kw) b = seg((0, -100, gap), (0, 100, gap), bar=2, group=2, **kw) return [a, b] def test_clash_found_when_bars_overlap(): hits = ck.find_clashes(crossing_pair(20.0)) # 25.4 - 20 = 5.4 관입 assert len(hits) == 1 assert hits[0]["penetration"] == pytest.approx(5.4) def test_no_clash_when_touching(): assert ck.find_clashes(crossing_pair(25.4)) == [] def test_no_clash_below_min_penetration(): # 관입 0.4mm 는 기준 0.5mm 이하라서 간섭이 아님 assert ck.find_clashes(crossing_pair(25.0)) == [] def test_clash_when_just_above_min_penetration(): assert len(ck.find_clashes(crossing_pair(24.8))) == 1 # 관입 0.6 def test_min_penetration_can_be_changed(): assert len(ck.find_clashes(crossing_pair(25.0), min_penetration=0.1)) == 1 def test_different_radii_are_added(): a = seg((-100, 0, 0), (100, 0, 0), radius=14.3, bar=1, group=1) b = seg((0, -100, 20), (0, 100, 20), radius=9.55, bar=2, group=2) hits = ck.find_clashes([a, b]) assert hits[0]["penetration"] == pytest.approx(14.3 + 9.55 - 20.0) def test_same_bar_is_skipped(): a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=1) b = seg((0, -100, 10), (0, 100, 10), bar=1, group=2) assert ck.find_clashes([a, b]) == [] def test_same_group_is_skipped(): # 같은 박스 안의 뚜껑과 바닥 조각은 겹침이음이라 간섭으로 보지 않음 a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=7) b = seg((0, -100, 10), (0, 100, 10), bar=2, group=7) assert ck.find_clashes([a, b]) == [] def test_far_apart_bars_are_not_checked(): a = seg((0, 0, 0), (100, 0, 0), bar=1, group=1) b = seg((0, 0, 5000), (100, 0, 5000), bar=2, group=2) assert ck.find_clashes([a, b]) == [] def test_only_deepest_penetration_per_bar_pair_is_kept(): # 같은 두 철근이 두 곳에서 겹침 (관입 5.4 와 10.4) a1 = seg((-100, 0, 0), (0, 0, 0), bar=1, group=1) a2 = seg((0, 0, 0), (100, 0, 0), bar=1, group=1) b1 = seg((-50, -100, 20), (-50, 100, 20), bar=2, group=2) # a1 과 간격 20 -> 관입 5.4 b2 = seg((50, -100, 15), (50, 100, 15), bar=2, group=2) # a2 와 간격 15 -> 관입 10.4 hits = ck.find_clashes([a1, a2, b1, b2]) assert len(hits) == 1 assert hits[0]["penetration"] == pytest.approx(10.4) def test_bar_pair_reported_once_regardless_of_order(): a = seg((-100, 0, 0), (100, 0, 0), bar=5, group=1) b = seg((0, -100, 10), (0, 100, 10), bar=2, group=2) hits = ck.find_clashes([a, b]) assert len(hits) == 1 assert (hits[0]["bar_a"], hits[0]["bar_b"]) == (2, 5) def test_hit_has_marks_and_location(): a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=1, mark="C1") b = seg((0, -100, 20), (0, 100, 20), bar=2, group=2, mark="C8") h = ck.find_clashes([a, b])[0] assert (h["mark_a"], h["mark_b"]) == ("C1", "C8") # 위치 = 네 끝점의 평균 assert h["point"] == pytest.approx((0.0, 0.0, 10.0)) def test_parallel_bars_at_250_have_no_clash_and_run_fast(): segs = [] for i in range(300): segs.append(seg((i * 250.0, 0, 0), (i * 250.0, 5000, 0), bar=i, group=i)) start = time.time() assert ck.find_clashes(segs) == [] assert time.time() - start < 2.0 # ---- 마크 쌍별 요약 ---- def test_summarize_by_mark_pair(): hits = [ {"mark_a": "C1", "mark_b": "C8", "penetration": 3.0}, {"mark_a": "C8", "mark_b": "C1", "penetration": 7.0}, {"mark_a": "C2", "mark_b": "C2", "penetration": 1.5}, ] s = ck.summarize_by_mark(hits) assert s["C1|C8"] == {"count": 2, "max_penetration": 7.0} assert s["C2|C2"] == {"count": 1, "max_penetration": 1.5}