- 직선화 첫 클릭이 직선 안이면 직선 통째 고름(붉은 강조)으로 새던 갈림 지움 · 빈 곳 클릭 측점선에 붙음 - 이미 직선인 범위 = 부분 구간 고름(보라 띠 · 안내) · ▲▼ = 가운데 + 양 끝 기본 L 곡선 - 직선화 · 자유지정 끝 R 이 저장 자동 R 이면 지움 = 끝 곡선 L 20 · 사용자 R 은 둠 - 법정 미달 판정 오차 1e-3 짝(TS · 파이썬) — L 19.99998 거짓 미달 없앰 - 새 도우미 design_profile_range_band — 직선화 · 자유지정 범위 단계 실시간 띠 · 시작–끝 · 길이 글 - Esc = 시작만 취소 · 모드 유지 · 고름 풀기로 안 샘 - 시험 test_271_straighten_pick (거울 포함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
8.1 KiB
Python
197 lines
8.1 KiB
Python
# -*- 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
|
||
assert "straightenBetween(base, ctx.edits(), fromM, toM, ctx.savedRadii())" 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
|