# -*- coding: utf-8 -*- """271-1~3 B05 종단 [직선화] 측점 고름 · 끝 R 기본 L · [직선화] · [자유지정] 실시간 띠. 요구 271 · PLAN 461. 계산(`logic/B05_Profile_UI_Profile_Straighten.ts` · `_Tools.ts` · `design_profile_free.ts` · `design_profile_range_band.ts` · `_CurveLaw.ts`)은 Node 헬퍼(`helper_271_straighten_pick.cjs`)가 트랜스파일해 돌리고, 파이썬 짝(`build_alignment` · `vertical_curve_shortfalls`)은 같은 base · 편집으로 곡선 자리 · R · L · 미달 경고가 같은지 거울로 본다. ./venv/Scripts/python.exe -m pytest resources/tester/test_271_straighten_pick.py -q """ import json import os import subprocess import sys from pathlib import Path import numpy as np import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parent.parent sys.path.insert(0, str(ROOT)) from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( # noqa: E402 AlignmentPolicy, build_alignment, ) from B05_Profile.B05_Profile_Engine_Grade_CurveLaw import ( # noqa: E402 SHORTFALL_TOLERANCE_M, vertical_curve_shortfalls, ) def _read(relative: str) -> str: return (ROOT / relative).read_text(encoding="utf-8") @pytest.fixture(scope="module") def out(): proc = subprocess.run( ["node", os.path.join(HERE, "helper_271_straighten_pick.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def _at(curves, chainage): return next((c for c in curves if abs(c["at"] - chainage) < 1e-6), None) def test_직선화_끝_기본_L_사이_곡선_없음(out): curves = out["straight"]["curves"] assert not [c for c in curves if 40 < c["at"] < 200] assert _at(curves, 40)["l"] == pytest.approx(20) and _at(curves, 200)["l"] == pytest.approx(20) assert out["straight"]["radii"] == [] def test_이미_직선_범위_부분_구간_반대_방향_같음(out): assert out["sub"] == {"fromM": 80, "toM": 160, "nodes": [80, 100, 120, 140, 160]} assert out["subReverse"] == out["sub"] assert out["subBent"] is None and out["subMixed"] is None assert out["subOneSpan"] is None # 사이 꼭짓점 없음 = 꺾을 곳 없음 def test_부분_구간_꺾기_양_끝_L20_거짓_미달_없음(out): curves = out["tiltSub"]["curves"] for at in (80, 120, 160): assert _at(curves, at)["l"] == pytest.approx(20, abs=1e-3) assert not [c for c in curves if 80 < c["at"] < 160 and c["at"] != 120] assert out["tiltSub"]["shortfalls"] == [] def test_법정_판정_오차_짝(out): assert out["tolerance"] == SHORTFALL_TOLERANCE_M == 1e-3 assert out["nearMiss"] == [] assert len(out["realMiss"]) == 1 near = [{"chainage_m": 120, "l_m": 19.99998, "r_m": 800}] assert vertical_curve_shortfalls(near, 100, 20) == [] real = [{"chainage_m": 120, "l_m": 19.9, "r_m": 800}] assert vertical_curve_shortfalls(real, 100, 20) == out["realMiss"] def test_끝_저장_자동_R_지움_사용자_R_둠(out): assert out["autoEnds"]["radii"] == [] curves = out["autoEnds"]["curves"] assert _at(curves, 40)["l"] == pytest.approx(20) and _at(curves, 200)["l"] == pytest.approx(20) assert out["userEnd"]["radii"] == {"40.000": 600} assert out["noAuto"] == ["40.000", "200.000"] def test_BP_EP_곡선_없음(out): assert out["bpEp"] == [] assert out["tiltBp"][:2] == [40, 80] # 시점 0 엔 곡선 없음 · 가운데 · 끝만 def test_곡선_섞인_범위_안쪽_R_지움_양_끝_L20(out): assert out["mixed"]["radii"] == [] curves = out["mixed"]["curves"] assert not [c for c in curves if 100 < c["at"] < 260] assert _at(curves, 100)["l"] == pytest.approx(20) and _at(curves, 260)["l"] == pytest.approx(20) def test_자유지정_끝_자동_R_지움_사용자_R_둠(out): assert out["freeAuto"] == ["120.000"] assert out["freeUser"] == ["40.000", "120.000"] def test_자유지정_범위_실시간_띠_Esc_시작만(out): assert out["liveBand"] == [40, 120] and out["liveEdges"] == [40, 120] assert out["liveReverse"] == [20, 40] assert out["outside"] == {"band": None, "edges": [40]} # 그래프 밖 = 시작선만 assert "Esc 시작 취소" in out["hintPending"] assert out["escTaken"] and out["afterEsc"] == {"phase": "range", "edges": [], "closed": 0} assert out["fixedBand"] == [40, 160] and out["armedPhase"] == "armed" # 둘째 클릭 뒤 고정 assert out["escArmed"] and out["afterEscArmed"] == {"phase": "range", "closed": 1} def test_범위_띠_그림(out): assert out["bandShape"] == ["rect:80+160", "text:40.0–120.0m · 80.0m", "line:80", "line:240"] assert out["bandStart"] == ["line:80"] assert out["viewIdle"] == 0 and out["viewOff"] == 0 assert out["viewStart"] == ["line:80"] assert out["viewHover"] == out["bandShape"] assert out["viewLeave"] == ["line:80"] assert out["viewRemount"] == ["rect:80+40", "text:40.0–60.0m · 20.0m", "line:80", "line:120"] def test_도구_직선화_부분_구간_붉은_강조_아님(out): assert out["toolsNone"] is False assert out["pending"] == { "at": 40, "hint": "40.0m 선택 — 두 번째 측점을 고르세요 (Esc 시작 취소)", } assert out["cancel"] == {"taken": True, "pending": None, "mode": "straighten"} assert out["cancelAgain"] is False assert out["subPicked"]["red"] == [] and out["subPicked"]["sub"]["fromM"] == 80 assert out["subPicked"]["hint"] == "이미 직선 — ▲▼ 로 꺾으면 양 끝에 기본 L 곡선" assert out["tiltCall"] == ["tilt", 80, 160, 0.1] assert out["newStart"] == {"pending": 60, "sub": None} assert out["straightenCall"] == [60, 180] assert out["shiftRed"] == {"red": 1, "sub": None, "cancel": False} # [쉬프트] 붉은 강조 그대로 def test_파이썬_거울_곡선_자리_R_L_미달(out): base = out["mirror"]["base"] policy = AlignmentPolicy.from_dict(base["policy"]) for case in out["mirror"]["cases"]: got = build_alignment( base_s=np.array([n["chainage_m"] for n in base["basePvi"]]), base_z=np.array([n["elevation_m"] for n in base["basePvi"]]), chainage=np.array(base["chainage"]), ground=np.array(base["ground"]), stations=base["stations"], policy=policy, edits=case["edits"], ) py = [[c["chainage_m"], c["r_m"], c["l_m"]] for c in got["curves"]] ts = case["curves"] assert [c[0] for c in py] == pytest.approx([c[0] for c in ts], abs=1e-6), case["name"] assert [c[1] for c in py] == pytest.approx([c[1] for c in ts], rel=1e-6), case["name"] assert [c[2] for c in py] == pytest.approx([c[2] for c in ts], abs=1e-6), case["name"] assert not [w for w in got["warnings"] if "미달" in w], case["name"] def test_배선(): panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts") assert "tools.handleStationPick(station ?? chainageM)" in panel assert "straightSubRun(alignment, fromM, toM)" in panel # 511-A — 인자 한 벌(계산 자리 길이 함수째 받음) args = ( "Parameters = [\n base,\n ctx.edits(),\n" " fromM,\n toM,\n ctx.savedRadii()," ) assert args in panel assert "autoRadii: ctx.savedRadii" in panel assert 'window.addEventListener("keydown", onEscape, true)' in panel assert "createRangeBandView(" in panel view = _read("A00_Common/design/design_profile_free_view.ts") assert "appendRangeBand(svg, x, heightPx" in view css = _read("B05_Profile/B05_Profile_UI_Style_Table.css") rule = css[css.index(".b05-profile-freemark__span {") :] rule = rule[: rule.index("}")] assert "--color-primary" in rule and "--color-surface" in rule # 밝음 · 어두움 토큰 def test_줄_수(): for relative in ( "B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts", "B05_Profile/B05_Profile_UI_Profile_Tools.ts", "B05_Profile/logic/B05_Profile_UI_Profile_Straighten.ts", "A00_Common/design/logic/design_profile_free.ts", "A00_Common/design/design_profile_free_view.ts", "A00_Common/design/design_profile_range_band.ts", ): assert len(_read(relative).splitlines()) <= 700