"""centerline 테스트 (Rhino 없이 실행됩니다). 단면 외곽선을 안쪽으로 피복만큼 옮긴 직선들의 교점으로 철근 중심선 꺾임점을 구합니다. 2D 좌표는 (s, z): s = 길이방향, z = 높이. 기준: 기존 프로젝트 rebar_pipeline/rhino/02_build_shape_VII.py (형식 VII, 도면 b=4167.1, c=3158.8) """ import math import pytest import centerline as cl # ---- 직선 오프셋 ---- def test_offset_line_right_side_of_rightward_segment_is_below(): # +s 방향으로 가는 선분의 오른쪽은 아래(-z) point, direction = cl.offset_line((0.0, 0.0), (100.0, 0.0), 10.0, "right") assert point == pytest.approx((0.0, -10.0)) assert direction == pytest.approx((1.0, 0.0)) def test_offset_line_left_side_of_rightward_segment_is_above(): point, direction = cl.offset_line((0.0, 0.0), (100.0, 0.0), 10.0, "left") assert point == pytest.approx((0.0, 10.0)) def test_offset_line_downward_segment_right_side_is_minus_s(): # 아래로 가는 선분의 오른쪽은 -s 쪽 (끝면 s=12100 에서 안쪽) point, _ = cl.offset_line((12100.0, 10738.0), (12100.0, 9238.0), 120.0, "right") assert point[0] == pytest.approx(11980.0) def test_offset_line_direction_is_unit_length(): _, d = cl.offset_line((0.0, 0.0), (30.0, 40.0), 5.0, "left") assert math.hypot(*d) == pytest.approx(1.0) def test_offset_line_rejects_zero_length_segment(): with pytest.raises(ValueError): cl.offset_line((1.0, 1.0), (1.0, 1.0), 10.0, "left") def test_offset_line_rejects_bad_side(): with pytest.raises(ValueError): cl.offset_line((0.0, 0.0), (1.0, 0.0), 10.0, "up") # ---- 직선 교점 ---- def test_line_intersection_perpendicular_lines(): a = ((0.0, 5.0), (1.0, 0.0)) b = ((7.0, 0.0), (0.0, 1.0)) assert cl.line_intersection(a, b) == pytest.approx((7.0, 5.0)) def test_line_intersection_diagonal(): a = ((0.0, 0.0), (1.0, 1.0)) b = ((10.0, 0.0), (-1.0, 1.0)) assert cl.line_intersection(a, b) == pytest.approx((5.0, 5.0)) def test_line_intersection_parallel_raises(): a = ((0.0, 0.0), (1.0, 0.0)) b = ((0.0, 5.0), (1.0, 0.0)) with pytest.raises(ValueError): cl.line_intersection(a, b) # ---- 중심선 꺾임점: 단순한 경우 ---- def test_centerline_of_u_shape_inside_a_box(): # 바깥 외곽: 위에서 왼쪽 벽을 따라 내려가 바닥을 지나 오른쪽 벽을 올라가는 ㄷ자 경로 segs = [ ((0.0, 100.0), (0.0, 0.0)), # 왼쪽 벽, 아래로 ((0.0, 0.0), (200.0, 0.0)), # 바닥, 오른쪽으로 ((200.0, 0.0), (200.0, 100.0)), # 오른쪽 벽, 위로 ] pts = cl.centerline_points(segs, 10.0, "left") # 아래로 가는 선분의 왼쪽은 +s, 오른쪽으로 가는 선분의 왼쪽은 +z (안쪽) assert pts[0] == pytest.approx((10.0, 100.0)) assert pts[1] == pytest.approx((10.0, 10.0)) assert pts[2] == pytest.approx((190.0, 10.0)) assert pts[3] == pytest.approx((190.0, 100.0)) def test_centerline_fixed_start_and_end_lengths(): segs = [ ((0.0, 100.0), (0.0, 0.0)), ((0.0, 0.0), (200.0, 0.0)), ((200.0, 0.0), (200.0, 100.0)), ] pts = cl.centerline_points(segs, 10.0, "left", start_len=30.0, end_len=40.0) assert pts[0] == pytest.approx((10.0, 40.0)) # 첫 꺾임점(10,10)에서 위로 30 assert pts[-1] == pytest.approx((190.0, 50.0)) # 마지막 꺾임점(190,10)에서 위로 40 assert len(pts) == 4 def test_centerline_single_segment_is_just_offset(): pts = cl.centerline_points([((0.0, 0.0), (100.0, 0.0))], 10.0, "left") assert pts == [pytest.approx((0.0, 10.0)), pytest.approx((100.0, 10.0))] def test_centerline_single_segment_rejects_fixed_lengths(): with pytest.raises(ValueError): cl.centerline_points([((0.0, 0.0), (100.0, 0.0))], 10.0, "left", start_len=5.0) def test_centerline_parallel_neighbors_raise(): segs = [((0.0, 0.0), (100.0, 0.0)), ((100.0, 0.0), (200.0, 0.0))] with pytest.raises(ValueError): cl.centerline_points(segs, 10.0, "left") def test_centerline_rejects_empty(): with pytest.raises(ValueError): cl.centerline_points([], 10.0, "left") # ---- 길이와 꺾임각 ---- def test_segment_lengths_and_total(): pts = [(0.0, 0.0), (3.0, 4.0), (3.0, 10.0)] assert cl.segment_lengths(pts) == pytest.approx([5.0, 6.0]) assert cl.polyline_length(pts) == pytest.approx(11.0) def test_turn_angles_right_angle(): pts = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)] assert cl.turn_angles(pts) == pytest.approx([90.0]) def test_turn_angles_straight_is_zero(): pts = [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)] assert cl.turn_angles(pts) == pytest.approx([0.0]) # ---- 형식 VII (대호지교 P1 C8/C10) 도면 값 재현 ---- P1_VII_SEGMENTS = [ ((12100.0, 10738.0), (12100.0, 9238.0)), # 끝면, 아래로 ((12100.0, 9238.0), (8100.0, 7738.0)), # 끝헌치 ((8100.0, 7738.0), (4900.0, 7738.0)), # 바닥 ((4900.0, 7738.0), (3400.0, 8238.0)), # 내측헌치 ] def test_shape_vii_matches_drawing_b_and_c(): pts = cl.centerline_points(P1_VII_SEGMENTS, 120.0, "right", start_len=630.0, end_len=2331.0) lengths = cl.segment_lengths(pts) assert len(pts) == 5 assert lengths[0] == pytest.approx(630.0) # a assert lengths[1] == pytest.approx(4167.1, abs=0.1) # b (도면) assert lengths[2] == pytest.approx(3158.8, abs=0.1) # c (도면) assert lengths[3] == pytest.approx(2331.0) # d def test_shape_vii_key_points(): pts = cl.centerline_points(P1_VII_SEGMENTS, 120.0, "right", start_len=630.0, end_len=2331.0) assert pts[0] == pytest.approx((11980.0, 9951.16), abs=0.01) # A assert pts[1] == pytest.approx((11980.0, 9321.16), abs=0.01) # 끝면-끝헌치 교점 assert pts[2][1] == pytest.approx(7858.0) # 바닥 위 120 (7738+120) assert pts[3][1] == pytest.approx(7858.0) assert pts[3][0] == pytest.approx(4919.47, abs=0.01) def test_shape_vii_cover_is_120_from_every_face(): # 중심선 위의 각 꺾임점은 이웃한 두 면에서 모두 120 떨어져 있어야 합니다. pts = cl.centerline_points(P1_VII_SEGMENTS, 120.0, "right", start_len=630.0, end_len=2331.0) for i, (p0, p1) in enumerate(P1_VII_SEGMENTS[:-1]): vertex = pts[i + 1] for seg in (P1_VII_SEGMENTS[i], P1_VII_SEGMENTS[i + 1]): assert cl.distance_to_line(vertex, seg[0], seg[1]) == pytest.approx(120.0, abs=1e-6) def test_distance_to_line_basic(): assert cl.distance_to_line((5.0, 7.0), (0.0, 0.0), (10.0, 0.0)) == pytest.approx(7.0)