chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,197 @@
|
||||
"""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}
|
||||
Reference in new issue
Block a user