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:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

+197
View File
@@ -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}