Files
Aislo/resources/tester/test_178_2_berm_fill_mirror.py
T
eomsangdonandClaude Opus 5.5 cb7a140c10 feat(B06): 소단 = 좌측 깊이 칸 숨김 · 횡단도 소단 고름 ▲▼ 만 · 측점 · 쪽마다 깊이 patch · 고름 조각 키 · 좌측 소단 상자 켬(194-1 · 194-2)
- 정의 「상세 내용」 = 폭만 · depth_m 표시 끔(옛 구간 값 = 기본값으로 읽음 · 새 기본 1 m)
- 짝: BermSpec 쪽마다 깊이 · side_berm ↔ sideBerm · 설계 결과 berm_depth_{side}_m 되싣기(파이썬 ↔ TS)
- 저장: patch berm_depth_{side}_m(≥ 0) · 재계산 살림 목록 짝(RD ↔ CR) · 세션 bermdepth
- 서버 [저장] 다시 셈 = 구조물 정본 소단 구간(load_berm_spans) + 측점 깊이 · 구간 지우면 소단 없음
- 고름: 소단 수평 단 = 높이만(▲ 0 에서 멈춤 · ▼ 사면 끝 앞 멈춤 · ◀▶ 막힘) · 사면 = 가로 · 노면 = 종단
- 고름을 조각 키(갈래 + 쪽 + 차례)로 기억 — 옮겨 다시 그려도 같은 조각
- 새 시험 test_194_1_berm_depth + helper · test_178_2 · test_54_26 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
2026-10-10 13:59:43 +09:00

209 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""178-2 소단 = 성토 사면 노면 끝 아래 depth_m 에 수평 한 단 · 아래로 성토 이음 — 파이썬 · TS 거울.
짝: `common_util_cross_berm.py` ↔ `.ts` · `B06_Section_Engine_Design_Geometry.py` ↔
`common_util_cross_design_geometry.ts`. 절토 소단은 끔 · 깊이 0 미만 = 0(도로보다 높을 수 없음).
"""
import json
import re
import subprocess
from pathlib import Path
import pytest
from B06_Section.B06_Section_Engine_Design import compute_cross_design
from common_util.common_util_cross_berm import BermSpec, fill_profile_points
from config.config_system import STANDARD_CROSS_SECTION
ROOT = Path(__file__).resolve().parents[2]
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
def _ground(slope: float) -> list[dict]:
return [
{"offset_m": o / 2, "elevation_m": round(100.0 + slope * o / 2, 4), "valid": True}
for o in range(-40, 41)
]
def _case(name, slope, z, mode, berm):
return {"name": name, "samples": _ground(slope), "z": z, "mode": mode, "berm": berm}
# 좌절토 = 우측 성토(지반이 오른쪽으로 내려감)
CASES = [
_case("깊이 1.0 · 폭 0.5", 0.6, 100.0, "left_cut", (0.5, 3.0, 0.0, 1.0)),
_case("깊이 0 = 노면 끝 높이", 0.6, 100.0, "left_cut", (0.5, 3.0, 0.0, 0.0)),
_case("깊이 음수 = 0", 0.6, 100.0, "left_cut", (0.5, 3.0, 0.0, -2.0)),
_case("깊이 > 사면 높이 = 소단 없음", 0.15, 100.0, "left_cut", (0.5, 3.0, 0.0, 5.0)),
_case("안쪽 기울기 5°", 0.6, 100.0, "left_cut", (1.0, 3.0, 5.0, 1.5)),
_case("소단 없음", 0.6, 100.0, "left_cut", None),
]
def _python(case):
berm = BermSpec(*case["berm"]) if case["berm"] else None
return compute_cross_design(
case["samples"],
case["z"],
standard=STANDARD_CROSS_SECTION,
ground_type="soil",
section_mode=case["mode"],
ditch_side="left",
berm=berm,
)
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import { computeCrossDesign } from "./common_util_cross_design.js";
import { fillProfilePoints } from "./common_util_cross_berm.js";
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
const designs = input.cases.map((c) => computeCrossDesign(c.samples, c.z, {
standard: input.standard, groundType: "soil", sectionMode: c.mode, ditchSide: "left",
berm: c.berm ? { widthM: c.berm[0], intervalM: c.berm[1], slopeDeg: c.berm[2], depthM: c.berm[3] } : null,
}));
const profile = fillProfilePoints(4, 99.9, 1.5, { widthM: 0.5, intervalM: 3, slopeDeg: 0, depthM: 1 }, 20, 100);
writeFileSync(process.argv[3], JSON.stringify({ designs, profile }));
"""
@pytest.fixture(scope="module")
def ts_results(tmp_path_factory):
if not TSC.is_file():
pytest.skip("프론트엔드 의존성 미설치")
tmp = tmp_path_factory.mktemp("berm")
out = tmp / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(ROOT / "common_util" / "common_util_cross_design.ts"),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
],
cwd=str(ROOT),
check=True,
capture_output=True,
)
for emitted in out.glob("*.js"):
emitted.write_text(
re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
src, dst = tmp / "in.json", tmp / "out.json"
src.write_text(
json.dumps({"cases": CASES, "standard": STANDARD_CROSS_SECTION}, ensure_ascii=False),
encoding="utf-8",
)
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(src), str(dst)], check=True, capture_output=True
)
return json.loads(dst.read_text(encoding="utf-8"))
def _flats(design: dict) -> list[tuple[float, float, float]]:
"""성토(오른쪽 · 음수 오프셋) 노면 끝 밖의 평탄 · 완만 도막 [(안 오프셋, 바깥 오프셋, 표고)]."""
edge = design["road_edges"]["right"]["offset_m"]
line = sorted(design["design_line"], key=lambda p: -p["offset_m"]) # 노면 → 바깥
found: list[tuple[float, float, float]] = []
for a, b in zip(line, line[1:], strict=False):
run = a["offset_m"] - b["offset_m"]
if a["offset_m"] > edge + 1e-6 or run <= 1e-6:
continue
if abs(b["elevation_m"] - a["elevation_m"]) / run < 0.1: # 소단(≤ 5°) · 지반 0.15 는 빠짐
if found and abs(found[-1][1] - a["offset_m"]) < 1e-6: # 이어진 도막은 하나로
found[-1] = (found[-1][0], b["offset_m"], found[-1][2])
else:
found.append((a["offset_m"], b["offset_m"], a["elevation_m"]))
return found
def _close(left, right, path=""):
if isinstance(left, dict):
assert set(left) == set(right), f"{path} 키 다름"
for key in left:
_close(left[key], right[key], f"{path}.{key}")
elif isinstance(left, list):
assert len(left) == len(right), f"{path} 길이 {len(left)} vs {len(right)}"
for i, (a, b) in enumerate(zip(left, right, strict=True)):
_close(a, b, f"{path}[{i}]")
elif isinstance(left, (int, float)) and not isinstance(left, bool):
assert abs(float(left) - float(right)) <= 1e-9, f"{path}: {left} vs {right}"
else:
assert left == right, f"{path}: {left!r} vs {right!r}"
def test_파이썬_TS_설계선_같음(ts_results):
for case, ts in zip(CASES, ts_results["designs"], strict=True):
_close(_python(case), ts, case["name"])
def test_사면_꼭짓점_거울():
py = fill_profile_points(4, 99.9, 1.5, BermSpec(0.5, 3, 0, 1), 20, top_z=100)
assert py[1] == pytest.approx((4 + 0.9 * 1.5, 99.0)) # 노면 끝 100 − 1.0
assert py[2] == pytest.approx((4 + 0.9 * 1.5 + 0.5, 99.0)) # 폭 0.5 수평
assert len(py) == 4 # 한 단만 · 그 아래 같은 경사로 끝까지
def test_사면_꼭짓점_TS_같음(ts_results):
py = fill_profile_points(4, 99.9, 1.5, BermSpec(0.5, 3, 0, 1), 20, top_z=100)
_close([list(p) for p in py], ts_results["profile"], "profile")
def test_한_단_깊이_폭():
by = {case["name"]: _python(case) for case in CASES}
edge_z = by["깊이 1.0 · 폭 0.5"]["road_edges"]["right"]["elevation_m"]
flats = _flats(by["깊이 1.0 · 폭 0.5"])
assert len(flats) == 1
inner, outer, z = flats[0]
assert z == pytest.approx(edge_z - 1.0, abs=1e-3)
assert inner - outer == pytest.approx(0.5, abs=1e-3)
assert by["깊이 1.0 · 폭 0.5"]["berm"]["depth_m"] == 1.0
zero = _flats(by["깊이 0 = 노면 끝 높이"])
assert len(zero) == 1 and zero[0][2] == pytest.approx(edge_z, abs=1e-3)
assert by["깊이 음수 = 0"]["design_line"] == by["깊이 0 = 노면 끝 높이"]["design_line"]
assert by["깊이 음수 = 0"]["berm"]["depth_m"] == 0.0
assert _flats(by["깊이 > 사면 높이 = 소단 없음"]) == []
assert _flats(by["소단 없음"]) == []
# 소단이 선 만큼 성토 사면이 바깥으로 밀려 성토 면적이 늘어남
assert by["깊이 1.0 · 폭 0.5"]["fill_area_m2"] > by["소단 없음"]["fill_area_m2"]
def test_절토_소단_끔():
"""좌측 절토 사면은 소단을 놓아도 소단 없는 단면과 같음."""
with_berm = _python(CASES[0])
plain = _python(CASES[-1])
left = [p for p in with_berm["design_line"] if p["offset_m"] > 0]
assert left == [p for p in plain["design_line"] if p["offset_m"] > 0]
assert with_berm["cut_area_m2"] == plain["cut_area_m2"]
def test_저장분_깊이_읽기():
"""[저장] 재계산이 읽는 저장분 — 없으면 기본 1.0 · 음수는 0."""
from B06_Section.B06_Section_Router_Design import stored_berm
assert stored_berm({"berm": {"width_m": 0.5}}).depth_m == 1.0
assert stored_berm({"berm": {"width_m": 0.5, "depth_m": -1}}).depth_m == 0.0
assert stored_berm({"berm": {"width_m": 0.5, "depth_m": 2.5}}).depth_m == 2.5
def test_좌측_패널_칸_구간_깊이_나름():
"""옛 구조물 옵션 depth_m → 세션 구간 → 측점 제원(기본값) · 서버 payload(같은 꼴).
좌측 칸은 194-1 에서 숨김 — 옮기기는 횡단도 ▲▼(`test_194_1_berm_depth`)."""
source = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
assert "options.depth_m" in source and "Math.max(rawDepth, 0)" in source
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
assert "depthM: spec.depth_m" in refresh