Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
201 lines
8.4 KiB
Python
201 lines
8.4 KiB
Python
"""294 물넘이 낮은 쪽 연장 · 도수로/돌붙임 끝단 · 띠 ↔ 성토 사면 고름(요구 188-2 · 188-3 · 189 · 그림_188).
|
||
|
||
못박는 것
|
||
- 294-3 물넘이 측점 낮은 쪽 노면 끝 원지반이 노면보다 높으면(절토) 노면을 같은 기울기로 늘여 원지반과
|
||
처음 만나는 자리까지(그 쪽 절토 사면 없음 · `tilt_reach`) · 성토 쪽 그대로 · 반폭 안에서 못 만나면 옛 길 ·
|
||
서버 정본 ↔ TS 짝 같은 값 = 횡단 설계 거울(`test_b06_cross_design_mirror` 「물넘이포장 낮은 쪽 노면
|
||
연장」) · 그림 = 콘크리트(포장층)가 늘인 끝까지.
|
||
- 294-4 도수로/돌붙임 띠 = 낮은 쪽 끝단(늘인 끝 · 아니면 유출 쪽 노견 끝)에서 성토 사면 따라 · 길이 비움 =
|
||
원지반 닿으면 끝 · 양측 절토 물넘이 = 띠 없음 · 길이 = 그 길이(사면보다 길면 원지반 따라) · 다른 구조물
|
||
(배관 · 박스 · 세월교) 같은 규칙(`chuteRun` · 원지반 `layout.ground`) + 같은 「길이 (m)」 칸 · 면적 = 늘
|
||
손 입력(301-1 · 길이 · 폭으로 안 셈) · 길이는 집계 · B08 수량에 안 실림.
|
||
|
||
진짜 TS 는 `helper_294_3_ford_extend_chute.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
HERE = Path(__file__).resolve().parent
|
||
ROOT = HERE.parents[1]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
if str(HERE) not in sys.path:
|
||
sys.path.insert(0, str(HERE))
|
||
|
||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||
from test_b06_cross_design_mirror import CASES, _dip_ground # noqa: E402
|
||
|
||
KW = {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"paved": True,
|
||
"tilted": True,
|
||
"tilt_pct": 5.0,
|
||
"standard": STANDARD_CROSS_SECTION,
|
||
}
|
||
|
||
pytestmark = pytest.mark.skipif(
|
||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||
reason="프론트엔드 의존성 미설치",
|
||
)
|
||
|
||
|
||
def _line(samples_fn) -> list[dict]:
|
||
return [
|
||
{"offset_m": o / 2, "elevation_m": round(samples_fn(o / 2), 4), "valid": True}
|
||
for o in range(-24, 25)
|
||
]
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def dip() -> dict:
|
||
return compute_cross_design(_dip_ground(), 100.4, **KW)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def ts(dip, tmp_path_factory) -> dict:
|
||
source = tmp_path_factory.mktemp("s294") / "in.json"
|
||
fill_samples = _line(lambda o: 100.0 + 0.35 * o)
|
||
fill = compute_cross_design(fill_samples, 100.4, **KW)
|
||
payload = {
|
||
"design": dip,
|
||
"dipSamples": _dip_ground(),
|
||
"fillSamples": fill_samples,
|
||
"fillDesign": fill,
|
||
}
|
||
source.write_text(json.dumps(payload), encoding="utf-8")
|
||
proc = subprocess.run(
|
||
["node", str(HERE / "helper_294_3_ford_extend_chute.cjs"), str(source)],
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
timeout=60,
|
||
)
|
||
assert proc.returncode == 0, proc.stderr
|
||
return json.loads(proc.stdout)
|
||
|
||
|
||
def test_양측_절토_낮은_쪽_늘인_끝은_원지반_교점(dip):
|
||
reach = dip["tilt_reach"]
|
||
# 원지반 101.05 + 0.2·o(−4.5 까지) · 노면 100.4 + 0.05·o → −4.3333 에서 만남
|
||
assert reach["side"] == "right"
|
||
assert reach["offset_m"] == pytest.approx(-4.3333, abs=1e-4)
|
||
assert reach["elevation_m"] == pytest.approx(100.4 - 0.05 * 4.3333, abs=1e-4)
|
||
edge = dip["road_edges"]["right"]["offset_m"]
|
||
for p in dip["design_line"]: # 노견 끝 ~ 늘인 끝 = 노면 기울기 그대로(절토 사면 없음)
|
||
if reach["offset_m"] - 1e-6 <= p["offset_m"] <= edge:
|
||
assert p["elevation_m"] == pytest.approx(100.4 + 0.05 * p["offset_m"], abs=1e-4)
|
||
assert dip["ditch_enabled"] is False
|
||
|
||
|
||
def test_성토_쪽은_그대로():
|
||
got = compute_cross_design(_line(lambda o: 100.0 + 0.35 * o), 100.4, **KW)
|
||
assert "tilt_reach" not in got # 낮은 쪽(우) 노면 끝 원지반이 노면 아래 = 성토 사면
|
||
|
||
|
||
def test_반폭_안에서_못_만나면_옛_길():
|
||
got = compute_cross_design(_line(lambda o: 101.0 + 0.02 * o), 100.4, **KW)
|
||
assert "tilt_reach" not in got # 늘인 노면이 끝까지 원지반 아래 = 절토 사면 그대로
|
||
right = [
|
||
p for p in got["design_line"] if p["offset_m"] < got["road_edges"]["right"]["offset_m"]
|
||
]
|
||
assert any(p["elevation_m"] > got["road_edges"]["right"]["elevation_m"] for p in right)
|
||
|
||
|
||
def test_서버_TS_같은_값은_거울_시험():
|
||
assert any(case["name"] == "물넘이포장 낮은 쪽 노면 연장" for case in CASES)
|
||
|
||
|
||
def test_콘크리트는_늘인_끝까지(dip, ts):
|
||
assert ts["edges"]["right"] == {
|
||
"offset_m": dip["tilt_reach"]["offset_m"],
|
||
"elevation_m": dip["tilt_reach"]["elevation_m"],
|
||
}
|
||
first = ts["pavePoints"].split()[1] # 포장층 윗면 우 끝
|
||
assert float(first.split(",")[0]) == pytest.approx(-4.3333, abs=1e-4)
|
||
|
||
|
||
def test_띠_길이_맞춤_공용(ts):
|
||
assert ts["runNone"] == [[[0, 0], [3, -4]]] # 길이 없음 = 원지반 닿으면 끝(그대로)
|
||
assert ts["runShort"] == [[[0, 0], [1.2, -1.6]]] # 길이 2 = 사면 따라 2 m
|
||
assert ts["runLong"] == [[[0, 0], [3, -4]], [[3, -4], [5, -4]]] # 사면 5 + 원지반 2
|
||
assert ts["runPointOnly"] == [] # 끝단이 이미 원지반 · 길이 없음 = 안 그림
|
||
assert ts["runPointLength"] == [[[3, -4], [5, -4]]]
|
||
|
||
|
||
def test_양측_절토_물넘이_띠_없음(ts):
|
||
assert len(ts["dipLine"]) == 1 # 늘인 끝이 원지반
|
||
assert ts["dipLine"][0]["offset"] == pytest.approx(-4.3333, abs=1e-4)
|
||
assert ts["dipChute"] == []
|
||
tail = ts["dipChuteLength"] # 길이를 넣으면 늘인 끝에서 원지반 따라 그 길이
|
||
assert tail[0]["offset"] == pytest.approx(-4.3333, abs=1e-4) and len(tail) >= 2
|
||
|
||
|
||
def test_성토_쪽_띠는_원지반_닿으면_끝(ts):
|
||
line = ts["fillLine"]
|
||
end = line[-1]
|
||
assert end["elevation"] == pytest.approx(100.0 + 0.35 * end["offset"], abs=1e-3) # 원지반 위
|
||
assert len(ts["fillLineOld"]) > len(line) # 원지반을 모르면 내려가는 원지반까지 따라갔음
|
||
chute = ts["fillChuteLength"]
|
||
start, stop = chute[0], chute[-1]
|
||
assert (stop["offset"] - start["offset"]) ** 2 + (
|
||
stop["elevation"] - start["elevation"]
|
||
) ** 2 == pytest.approx(1.0, abs=1e-6)
|
||
|
||
|
||
def test_다른_구조물_같은_규칙과_길이_칸(ts):
|
||
assert ts["chuteItems"][-1] == "length_m"
|
||
assert ts["fordPaveChuteSame"] is True
|
||
hatch = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Wall_Hatch.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
assert "const lines = chuteRun(allLines, chute.length_m, ground);" in hatch
|
||
for name in ("Culvert", "Box", "Ford"):
|
||
overlay = (ROOT / "B06_Section" / f"B06_Section_UI_Cross_{name}.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
assert "layout.ground," in overlay
|
||
form = (ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
assert "syncChuteArea" not in form # 면적 = 손 입력(301-1)
|
||
|
||
|
||
def test_길이는_집계_수량에_안_실림():
|
||
from B06_Section.B06_Section_Engine_Culvert import _chute
|
||
from B08_Quantity.B08_Quantity_Router_Summary import _chutes
|
||
from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows
|
||
|
||
options = {
|
||
"chute_add_1_structure": "돌붙임",
|
||
"chute_add_1_bond": "찰붙임",
|
||
"chute_add_1_area": 12,
|
||
}
|
||
longer = {**options, "chute_add_1_length_m": 7}
|
||
assert _chute(longer)["length_m"] == 7 and "length_m" not in _chute(options)
|
||
|
||
class Sheet:
|
||
def __init__(self) -> None:
|
||
self.rows: list[tuple] = []
|
||
|
||
def add(self, at, key, sort, group, label, value, combos) -> None:
|
||
self.rows.append((key, label, value))
|
||
|
||
sheet = Sheet()
|
||
_chutes(sheet, 50.0, longer, {"gchuteadd1": "SS1"}, {})
|
||
assert sheet.rows == [("chute|찰붙임", "㎡", 12.0)]
|
||
item = {"type_id": "pipe", "options": options, "복사본": "pipe@50.0", "흙막이단": 0}
|
||
plain = container_rows(item, {"gchuteadd1": "SS1"})
|
||
long = container_rows({**item, "options": longer}, {"gchuteadd1": "SS1"})
|
||
for key in ("length_m", "inlet_revet_length_m", "outlet_revet_length_m", "area_m"):
|
||
assert plain[0]["options"].get(key) == long[0]["options"].get(key)
|