"""단면 외곽선에서 철근 중심선 꺾임점을 계산하는 모듈. Rhino 없이 실행됩니다. 단위는 mm, 각도는 도(degree)입니다. 2D 좌표는 (s, z): s = 길이방향, z = 높이 (frame.py 의 로컬 좌표와 같은 방향). 방법 (기존 프로젝트 rebar_pipeline/README.md §3): 1. 단면 외곽의 각 면을 안쪽으로 피복만큼 옮긴 직선으로 바꿉니다. 2. 이웃한 두 직선의 교점이 철근 중심선의 꺾임점입니다. 3. 시작과 끝은 도면의 고정 길이만큼 연장합니다. """ import math EPS = 1e-9 def _unit_direction(p0, p1): dx = p1[0] - p0[0] dz = p1[1] - p0[1] length = math.hypot(dx, dz) if length < EPS: raise ValueError("길이가 0인 선분입니다: %r" % (p0,)) return (dx / length, dz / length) def _normal(direction, side): """진행 방향의 왼쪽 또는 오른쪽 단위 법선.""" dx, dz = direction if side == "left": return (-dz, dx) if side == "right": return (dz, -dx) raise ValueError("side 는 'left' 또는 'right' 여야 합니다: %r" % (side,)) def offset_line(p0, p1, distance, side): """선분 p0->p1 을 진행 방향의 side(left/right)로 distance 만큼 옮긴 직선. 돌려주는 값: (직선 위의 한 점, 진행 방향 단위벡터) """ direction = _unit_direction(p0, p1) n = _normal(direction, side) point = (p0[0] + n[0] * distance, p0[1] + n[1] * distance) return point, direction def line_intersection(line_a, line_b): """두 직선 (점, 방향) 의 교점. 평행하면 ValueError.""" (ax, az), (adx, adz) = line_a (bx, bz), (bdx, bdz) = line_b cross = adx * bdz - adz * bdx if abs(cross) < EPS: raise ValueError("두 직선이 평행해서 교점이 없습니다") t = ((bx - ax) * bdz - (bz - az) * bdx) / cross return (ax + adx * t, az + adz * t) def centerline_points(segments, cover, side, start_len=None, end_len=None): """철근 중심선의 꺾임점 목록 (s, z). segments : 외곽선 선분 목록 [(시작점, 끝점), ...]. 철근이 지나가는 순서와 방향대로 적습니다. cover : 피복 (철근 중심에서 면까지 거리) side : 콘크리트 안쪽이 진행 방향의 'left' 인지 'right' 인지 start_len: 첫 꺾임점에서 첫 선분 반대 방향으로 연장할 길이. None 이면 첫 선분의 시작점을 그대로 씁니다. end_len : 마지막 꺾임점에서 마지막 선분 방향으로 연장할 길이. None 이면 마지막 선분의 끝점을 그대로 씁니다. """ if not segments: raise ValueError("선분이 하나 이상 필요합니다") lines = [offset_line(p0, p1, cover, side) for (p0, p1) in segments] if len(lines) == 1: if start_len is not None or end_len is not None: raise ValueError("선분이 1개이면 고정 길이를 쓸 수 없습니다") (p0, d) = lines[0] length = math.hypot(segments[0][1][0] - segments[0][0][0], segments[0][1][1] - segments[0][0][1]) return [p0, (p0[0] + d[0] * length, p0[1] + d[1] * length)] vertices = [line_intersection(lines[i], lines[i + 1]) for i in range(len(lines) - 1)] if start_len is None: first = lines[0][0] else: d0 = lines[0][1] first = (vertices[0][0] - d0[0] * start_len, vertices[0][1] - d0[1] * start_len) if end_len is None: seg_end = segments[-1][1] n = _normal(lines[-1][1], side) last = (seg_end[0] + n[0] * cover, seg_end[1] + n[1] * cover) else: dn = lines[-1][1] last = (vertices[-1][0] + dn[0] * end_len, vertices[-1][1] + dn[1] * end_len) return [first] + vertices + [last] def segment_lengths(points): """꺾임점 목록의 각 구간 길이.""" return [math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(points, points[1:])] def polyline_length(points): """꺾임점을 이은 전체 길이.""" return sum(segment_lengths(points)) def turn_angles(points): """각 안쪽 꺾임점에서 진행 방향이 바뀌는 각도(도, 0~180). 직선이면 0, 직각이면 90.""" angles = [] for i in range(1, len(points) - 1): d1 = _unit_direction(points[i - 1], points[i]) d2 = _unit_direction(points[i], points[i + 1]) dot = max(-1.0, min(1.0, d1[0] * d2[0] + d1[1] * d2[1])) angles.append(math.degrees(math.acos(dot))) return angles def distance_to_line(point, line_p0, line_p1): """점에서 (선분이 아니라) 무한 직선 line_p0-line_p1 까지의 수직거리.""" d = _unit_direction(line_p0, line_p1) vx = point[0] - line_p0[0] vz = point[1] - line_p0[1] return abs(vx * d[1] - vz * d[0])