"""frame 테스트 (Rhino 없이 실행됩니다). 월드 좌표 (x, y, z) <-> 코핑 로컬 좌표 (s, t, z) s = 길이방향 (코핑 축), t = 폭방향 (s를 반시계로 90도 돌린 방향), z = 월드 높이 그대로 기존 프로젝트: rebar_pipeline/rhino/07_ctc_extract.py 의 OX, OY, ANG 방식과 같습니다. """ import math import pytest import frame as fr # ---- 기본 변환 ---- def test_identity_frame_keeps_coordinates(): f = fr.Frame(0.0, 0.0, 0.0) assert f.to_local(10.0, 20.0, 5.0) == pytest.approx((10.0, 20.0, 5.0)) def test_origin_maps_to_zero_zero(): f = fr.Frame(1000.0, 2000.0, 0.3) s, t, z = f.to_local(1000.0, 2000.0, 77.0) assert (s, t) == pytest.approx((0.0, 0.0)) assert z == 77.0 def test_z_is_not_changed(): f = fr.Frame(1000.0, 2000.0, 0.3) assert f.to_local(1500.0, 2500.0, 9331.0)[2] == 9331.0 assert f.to_world(10.0, 20.0, 9331.0)[2] == 9331.0 def test_angle_90_degrees_swaps_axes(): f = fr.Frame(0.0, 0.0, math.pi / 2) # 월드 +Y 방향이 로컬 +s 방향 assert f.to_local(0.0, 100.0, 0.0) == pytest.approx((100.0, 0.0, 0.0)) # 월드 -X 방향이 로컬 +t 방향 assert f.to_local(-50.0, 0.0, 0.0) == pytest.approx((0.0, 50.0, 0.0)) def test_angle_45_degrees(): f = fr.Frame(0.0, 0.0, math.pi / 4) r = math.sqrt(2.0) * 50.0 assert f.to_local(50.0, 50.0, 0.0) == pytest.approx((r, 0.0, 0.0)) def test_to_world_basic(): f = fr.Frame(100.0, 200.0, math.pi / 2) # 로컬 s=10 -> 월드 +Y 방향으로 10 assert f.to_world(10.0, 0.0, 0.0) == pytest.approx((100.0, 210.0, 0.0)) # 로컬 t=10 -> 월드 -X 방향으로 10 assert f.to_world(0.0, 10.0, 0.0) == pytest.approx((90.0, 200.0, 0.0)) # ---- 왕복 변환 (월드 -> 로컬 -> 월드가 원래 값이어야 함) ---- @pytest.mark.parametrize("point", [ (480708784.0, 153831202.0, 7738.0), (480721068.0, 153835457.0, -342.0), (0.0, 0.0, 0.0), (-5.5, 1234.5, 10.0), ]) def test_round_trip_world_local_world(point): f = fr.from_two_points((480708784.0, 153831202.0), (480721068.0, 153835457.0)) back = f.to_world(*f.to_local(*point)) assert back == pytest.approx(point, abs=1e-6) def test_round_trip_local_world_local(): f = fr.Frame(480714926.0, 153833329.5, 0.3333) local = (1234.5, -678.9, 10405.3) back = f.to_local(*f.to_world(*local)) assert back == pytest.approx(local, abs=1e-6) # ---- 두 기둥 중심으로 프레임 만들기 (대호지교 P1 값) ---- def test_from_two_points_origin_is_midpoint(): f = fr.from_two_points((480708784.0, 153831202.0), (480721068.0, 153835457.0)) assert f.origin_x == pytest.approx(480714926.0) assert f.origin_y == pytest.approx(153833329.5) def test_from_two_points_angle_matches_p1(): f = fr.from_two_points((480708784.0, 153831202.0), (480721068.0, 153835457.0)) assert f.angle == pytest.approx(math.atan2(4255.0, 12284.0)) assert math.degrees(f.angle) == pytest.approx(19.105, abs=0.001) def test_p1_columns_are_on_s_axis_at_half_spacing(): a = (480708784.0, 153831202.0) b = (480721068.0, 153835457.0) f = fr.from_two_points(a, b) sa, ta, _ = f.to_local(a[0], a[1], 0.0) sb, tb, _ = f.to_local(b[0], b[1], 0.0) # 기둥 중심간격 13,000 (좌표 반올림 때문에 0.1mm 이내) assert sa == pytest.approx(-6500.0, abs=0.1) assert sb == pytest.approx(6500.0, abs=0.1) assert ta == pytest.approx(0.0, abs=1e-6) assert tb == pytest.approx(0.0, abs=1e-6) def test_from_two_points_rejects_same_point(): with pytest.raises(ValueError): fr.from_two_points((1.0, 1.0), (1.0, 1.0)) # ---- 프레임 바꾸기 (언제든 다시 바꿀 수 있어야 함) ---- def test_convert_between_frames_matches_going_through_world(): f1 = fr.Frame(100.0, 200.0, 0.2) f2 = fr.Frame(-500.0, 40.0, 1.1) local1 = (321.0, -45.0, 12.0) via_world = f2.to_local(*f1.to_world(*local1)) assert fr.convert(local1, f1, f2) == pytest.approx(via_world, abs=1e-6) def test_convert_there_and_back_gives_original(): f1 = fr.Frame(100.0, 200.0, 0.2) f2 = fr.Frame(-500.0, 40.0, 1.1) local1 = (321.0, -45.0, 12.0) back = fr.convert(fr.convert(local1, f1, f2), f2, f1) assert back == pytest.approx(local1, abs=1e-6) def test_convert_same_frame_changes_nothing(): f = fr.Frame(100.0, 200.0, 0.2) assert fr.convert((1.0, 2.0, 3.0), f, f) == pytest.approx((1.0, 2.0, 3.0)) def test_new_origin_changes_local_values_but_not_world(): # 원점만 바꿔도 월드 위치는 그대로, 로컬 값만 달라집니다. world = (1000.0, 2000.0, 5.0) f1 = fr.Frame(0.0, 0.0, 0.3) f2 = fr.Frame(500.0, 500.0, 0.3) l1 = f1.to_local(*world) l2 = f2.to_local(*world) assert l1 != pytest.approx(l2) assert f1.to_world(*l1) == pytest.approx(world, abs=1e-6) assert f2.to_world(*l2) == pytest.approx(world, abs=1e-6) # ---- 방향 벡터 (이동 없이 회전만) ---- def test_direction_to_local_ignores_origin(): f = fr.Frame(1000.0, 2000.0, math.pi / 2) assert f.direction_to_local(0.0, 1.0, 0.0) == pytest.approx((1.0, 0.0, 0.0)) def test_direction_round_trip(): f = fr.Frame(1000.0, 2000.0, 0.7) d = (0.3, -0.8, 0.5) assert f.direction_to_world(*f.direction_to_local(*d)) == pytest.approx(d) # ---- 점 여러 개 ---- def test_to_local_many_matches_single_calls(): f = fr.Frame(10.0, 20.0, 0.4) pts = [(1.0, 2.0, 3.0), (100.0, -50.0, 7.0)] assert f.to_local_many(pts) == [f.to_local(*p) for p in pts] def test_to_world_many_matches_single_calls(): f = fr.Frame(10.0, 20.0, 0.4) pts = [(1.0, 2.0, 3.0), (100.0, -50.0, 7.0)] assert f.to_world_many(pts) == [f.to_world(*p) for p in pts]