"""철근 중심선 간섭 검사 모듈. Rhino 없이 실행됩니다. 단위는 mm. 좌표는 (x, y, z) 3D 점입니다. 계산을 정확하게 하려면 월드 좌표(약 4.8억)보다 frame.py 로 바꾼 로컬 좌표를 쓰는 것이 좋습니다. 규칙 (기존 프로젝트 rebar_pipeline/rhino/08_clash_check.py): - 철근을 중심선 선분으로 쪼개서, 두 선분의 최단거리 d 에 대해 관입 = rA + rB - d 가 min_penetration(기본 0.5mm)보다 크면 간섭입니다. - 같은 철근, 같은 그룹(겹침이음 모델링인 박스) 선분끼리는 제외합니다. - 철근 쌍마다 가장 깊은 관입 하나만 기록합니다. """ import math EPS = 1e-12 BBOX_MARGIN = 1.0 # 후보를 찾을 때 반지름에 더하는 여유(mm) class Seg(object): """철근 중심선의 선분 하나. p, q : 끝점 (x, y, z) radius : 철근 반지름 bar : 철근 번호 (같은 철근끼리는 검사 제외) group : 그룹 번호 (같은 박스의 뚜껑·바닥 조각처럼 검사 제외할 묶음) mark : 철근 마크 이름 (보고용) """ def __init__(self, p, q, radius, bar, group, mark=""): self.p = (float(p[0]), float(p[1]), float(p[2])) self.q = (float(q[0]), float(q[1]), float(q[2])) self.radius = float(radius) self.bar = bar self.group = group self.mark = mark def _sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) def _dot(a, b): return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] def _clamp01(x): if x < 0.0: return 0.0 if x > 1.0: return 1.0 return x def segment_distance(p1, q1, p2, q2): """두 선분 p1-q1, p2-q2 사이의 최단거리 (3D, 길이 0인 선분도 가능).""" d1 = _sub(q1, p1) d2 = _sub(q2, p2) r = _sub(p1, p2) a = _dot(d1, d1) e = _dot(d2, d2) f = _dot(d2, r) if a <= EPS and e <= EPS: s = 0.0 t = 0.0 elif a <= EPS: s = 0.0 t = _clamp01(f / e) else: c = _dot(d1, r) if e <= EPS: t = 0.0 s = _clamp01(-c / a) else: b = _dot(d1, d2) denom = a * e - b * b if denom > 1e-12 * a * e: s = _clamp01((b * f - c * e) / denom) else: s = 0.0 t = (b * s + f) / e if t < 0.0: t = 0.0 s = _clamp01(-c / a) elif t > 1.0: t = 1.0 s = _clamp01((b - c) / a) c1 = (p1[0] + d1[0] * s, p1[1] + d1[1] * s, p1[2] + d1[2] * s) c2 = (p2[0] + d2[0] * t, p2[1] + d2[1] * t, p2[2] + d2[2] * t) diff = _sub(c1, c2) return math.sqrt(_dot(diff, diff)) def _box(s): """선분을 반지름 + 여유만큼 부풀린 상자 (xmin, ymin, zmin, xmax, ymax, zmax).""" m = s.radius + BBOX_MARGIN return (min(s.p[0], s.q[0]) - m, min(s.p[1], s.q[1]) - m, min(s.p[2], s.q[2]) - m, max(s.p[0], s.q[0]) + m, max(s.p[1], s.q[1]) + m, max(s.p[2], s.q[2]) + m) def _boxes_overlap(a, b): return (a[0] <= b[3] and b[0] <= a[3] and a[1] <= b[4] and b[1] <= a[4] and a[2] <= b[5] and b[2] <= a[5]) def find_clashes(segments, min_penetration=0.5): """간섭 목록을 돌려줍니다 (관입이 깊은 것부터). 각 항목: {"bar_a", "bar_b", "mark_a", "mark_b", "penetration", "point"} bar_a < bar_b 로 정렬되고, point 는 간섭 선분 네 끝점의 평균입니다. """ boxes = [_box(s) for s in segments] order = sorted(range(len(segments)), key=lambda i: boxes[i][0]) best = {} for oi in range(len(order)): i = order[oi] a = segments[i] for oj in range(oi + 1, len(order)): j = order[oj] if boxes[j][0] > boxes[i][3]: break # x 방향으로 이미 떨어져 있음. 뒤쪽은 더 멀리 있음 b = segments[j] if a.bar == b.bar or a.group == b.group: continue if not _boxes_overlap(boxes[i], boxes[j]): continue d = segment_distance(a.p, a.q, b.p, b.q) pen = a.radius + b.radius - d if pen <= min_penetration: continue first, second = (a, b) if a.bar <= b.bar else (b, a) key = (first.bar, second.bar) old = best.get(key) if old is None or pen > old["penetration"]: point = ((a.p[0] + a.q[0] + b.p[0] + b.q[0]) / 4.0, (a.p[1] + a.q[1] + b.p[1] + b.q[1]) / 4.0, (a.p[2] + a.q[2] + b.p[2] + b.q[2]) / 4.0) best[key] = { "bar_a": first.bar, "bar_b": second.bar, "mark_a": first.mark, "mark_b": second.mark, "penetration": pen, "point": point, } hits = list(best.values()) hits.sort(key=lambda h: -h["penetration"]) return hits def summarize_by_mark(hits): """마크 쌍별 간섭 개수와 최대 관입. 키는 'C1|C8' 처럼 이름순으로 정렬된 마크 쌍.""" summary = {} for h in hits: names = sorted([h["mark_a"], h["mark_b"]]) key = names[0] + "|" + names[1] item = summary.get(key) if item is None: summary[key] = {"count": 1, "max_penetration": h["penetration"]} else: item["count"] += 1 if h["penetration"] > item["max_penetration"]: item["max_penetration"] = h["penetration"] return summary