- 얹기: attachShapes · attach_shapes 에 소단(section.berm · 덮는 소단 없음 = null) · SHAPE_KEYS 에 berm · TS attachBermSpecs ↔ 파이썬 attach_berm_specs · berm_spans_from 짝 - 읽기: 저장 노선 다시 셈(브라우저 · Node cross.refresh) = 얹힌 section.berm(칸 없음 = 저장분 design.berm) · 노선 초안 · 서버 Node 묶음 cross = crossWithShapes 의 얹힌 제원(bermAt · crossWithBerm 걷음) - 계단은 계산 안쪽 그대로 · [저장] 길(구조물 정본 → cross.berm_spans) 그대로 - 지움: 시설 줄 syncBerm · putBerm(C4.copy) · writeBermSpans · 세션 칸 berm · bermRequest · design_shared bermAt - 시험: test_272_berm_source 측점 얹기 거울로 · test_323_4 소단 얹음 · 477 · 511-A · 472 떠남 · 365 · 272 고름 헬퍼가 사본 대신 얹힌 소단 · design_pipeline_page 700 줄 → 699(주석 한 줄) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
8.2 KiB
Python
230 lines
8.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""467 · 577-2 (요구 272 · 434 · 437) — 소단 원천 한 벌 = 측점에 얹은 제원(옹벽처럼).
|
|
|
|
못박는 것
|
|
- 측점 얹기 — 구조물 → 측점마다 `section.berm`(덮는 소단 없음 = null · 겹치면 먼저 놓은 것) ·
|
|
TS `attachBermSpecs` ↔ 파이썬 `attach_berm_specs`.
|
|
- 다시 셈 = 얹힌 제원 · 칸 없음(아직 안 얹음) = 저장분 `design.berm` · null = 소단 없음 —
|
|
얹힌 값 = 저장분 값 = [저장] 서버 값(`enforce_design_lines`).
|
|
- 거울 — TS `bermSpansFromStructures` ↔ 파이썬 `load_berm_spans` ·
|
|
TS `storedBermSpec` ↔ 파이썬 `berm_from`.
|
|
- 세션 사본(`berm` 칸 · 시설 줄 `syncBerm`) 없음.
|
|
|
|
Node 헬퍼 `helper_272_berm_source.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
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))
|
|
|
|
from B06_Section.B06_Section_Engine_Structures_Wall import ( # noqa: E402
|
|
attach_berm_specs,
|
|
load_berm_spans,
|
|
)
|
|
from B06_Section.B06_Section_Router_Design import berm_from, enforce_design_lines # noqa: E402
|
|
from config.config_system import ( # noqa: E402
|
|
STANDARD_CROSS_SECTION,
|
|
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
|
)
|
|
|
|
|
|
def _berm(start, end, **options):
|
|
return {
|
|
"type_id": "berm",
|
|
"placement": "interval",
|
|
"start_m": start,
|
|
"end_m": end,
|
|
"options": options,
|
|
}
|
|
|
|
|
|
SAVED = [_berm(0.0, 100.0, width_m=1.0, interval_m=3.0, slope_deg=0.0, depth_m=1.0)]
|
|
STRUCTURE_CASES = [
|
|
SAVED,
|
|
[
|
|
_berm(50.0, 80.0, width_m=0), # 폭 0 = 없음
|
|
_berm(10.0, 30.0, depth_m=""), # 빈 깊이 = 기본 1.0
|
|
_berm(30.0, 40.0, depth_m=-2, width_m="0.7"), # 음수 깊이 = 0 · 글 수
|
|
_berm(60.0, 70.0, width_m="abc", interval_m=2.5, slope_deg=4), # 못 읽는 폭 = 기본
|
|
],
|
|
[{"type_id": "berm", "placement": "point", "chainage_m": 25.0, "options": {}}],
|
|
[],
|
|
]
|
|
STORED_CASES = [
|
|
{"width_m": 0.5},
|
|
{"width_m": 0.5, "depth_m": -1},
|
|
{},
|
|
{"width_m": "0.7", "interval_m": 2},
|
|
None,
|
|
{"width_m": None},
|
|
{"width_m": ""},
|
|
]
|
|
GROUND = [
|
|
{"offset_m": o / 2, "elevation_m": round(96.0 + 0.02 * o / 2, 4), "valid": True}
|
|
for o in range(-40, 41)
|
|
]
|
|
STORED_BERM = {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0}
|
|
CHAINAGES = [0.0, 10.0, 25.0, 30.0, 35.0, 55.0, 65.0, 100.0, 100.5, None]
|
|
LONG = {
|
|
"design_profiles": [
|
|
{
|
|
"samples": [
|
|
{"chainage_m": 0, "elevation_m": 100},
|
|
{"chainage_m": 200, "elevation_m": 100},
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
def _project(root: Path, structures: list[dict]) -> Path:
|
|
route = root / "B05_Profile" / "route"
|
|
route.mkdir(parents=True)
|
|
(route / "structures.json").write_text(
|
|
json.dumps({"revision": 1, "structures": structures}, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
return root
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def out(tmp_path_factory):
|
|
work = tmp_path_factory.mktemp("b272berm")
|
|
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
[
|
|
"node",
|
|
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
|
"build",
|
|
"--configLoader",
|
|
"runner",
|
|
"--ssr",
|
|
"../resources/tester/helper_272_berm_source_entry.ts",
|
|
"--outDir",
|
|
str(work),
|
|
"--logLevel",
|
|
"error",
|
|
],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=300,
|
|
)
|
|
assert build.returncode == 0, build.stderr
|
|
bundle = work / "bundle.mjs"
|
|
shutil.copy(work / "helper_272_berm_source_entry.js", bundle)
|
|
job = work / "job.json"
|
|
job.write_text(
|
|
json.dumps(
|
|
{
|
|
"standard": STANDARD_CROSS_SECTION,
|
|
"rockDefault": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
|
"structureCases": STRUCTURE_CASES,
|
|
"storedCases": STORED_CASES,
|
|
"ground": GROUND,
|
|
"storedBerm": STORED_BERM,
|
|
"chainages": CHAINAGES,
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
proc = subprocess.run( # noqa: S603
|
|
["node", str(HERE / "helper_272_berm_source.mjs"), str(bundle), str(job)],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=120,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_거울_구조물_소단_구간(out, tmp_path):
|
|
for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["spans"], strict=True)):
|
|
py = load_berm_spans(_project(tmp_path / f"c{index}", structures))
|
|
assert py is not None, index
|
|
want = [{"start_m": s, "end_m": e, **spec} for s, e, spec in py]
|
|
assert len(ts) == len(want), (index, ts, want)
|
|
for got, exp in zip(ts, want, strict=True):
|
|
assert got == pytest.approx(exp), (index, got, exp)
|
|
|
|
|
|
def test_거울_저장분_소단_제원(out):
|
|
for spec, ts in zip(STORED_CASES, out["stored"], strict=True):
|
|
py = berm_from(spec, {})
|
|
if py is None:
|
|
assert ts is None, spec
|
|
continue
|
|
want = {k: getattr(py, k) for k in ("width_m", "interval_m", "slope_deg", "depth_m")}
|
|
assert ts == pytest.approx(want), spec
|
|
|
|
|
|
def test_거울_측점_얹기(out, tmp_path):
|
|
for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["attach"], strict=True)):
|
|
spans = load_berm_spans(_project(tmp_path / f"a{index}", structures))
|
|
py = [{"chainage_m": c} for c in CHAINAGES]
|
|
attach_berm_specs(py, spans)
|
|
assert [("berm" in r) for r in ts] == [("berm" in r) for r in py], index
|
|
for got, exp in zip(ts, py, strict=True):
|
|
if exp.get("berm") is None:
|
|
assert got.get("berm") is None, (index, got, exp)
|
|
else:
|
|
assert got["berm"] == pytest.approx(exp["berm"]), (index, got, exp)
|
|
first = out["attach"][0]
|
|
assert first[1]["berm"] == pytest.approx(STORED_BERM) and first[8]["berm"] is None
|
|
assert "berm" not in first[-1] # 누가거리 없음 = 칸 없음
|
|
|
|
|
|
def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path):
|
|
r = out["refresh"]
|
|
no_copy, same, empty = r["unattached"], r["attachedSame"], r["attachedNone"]
|
|
assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # 안 얹음 = 저장분
|
|
assert empty["fill"] != no_copy["fill"] # 얹었는데 없음 = 소단 없음(저장분으로 안 물러섬)
|
|
# [저장] = 서버가 구조물 정본으로 다시 셈
|
|
root = _project(tmp_path / "save", SAVED)
|
|
design = {"ground_type": "soil", "section_mode": "both_fill", "ditch_type": "standard"}
|
|
sections = [{"chainage_m": 40.0, "samples": GROUND, "design": design}]
|
|
assert enforce_design_lines(LONG, sections, root) == 1
|
|
assert abs(sections[0]["design"]["fill_area_m2"] - same["fill"]) < 1e-3
|
|
bare = [{"chainage_m": 40.0, "samples": GROUND, "design": dict(design)}]
|
|
enforce_design_lines(LONG, bare, _project(tmp_path / "bare", []))
|
|
assert abs(bare[0]["design"]["fill_area_m2"] - empty["fill"]) < 1e-3
|
|
|
|
|
|
def test_사본_없음():
|
|
for rel in (
|
|
"A00_Common/design/design_facility_lane.ts",
|
|
"A00_Common/b_page_state.ts",
|
|
"B06_Section/B06_Section_UI_Page_Persist.ts",
|
|
"B06_Section/B06_Section_Cross_Refresh.ts",
|
|
"A00_Common/design/design_shared.ts",
|
|
):
|
|
text = (ROOT / rel).read_text(encoding="utf-8")
|
|
for word in (
|
|
"writeBermSpans",
|
|
"syncBerm",
|
|
"putBerm",
|
|
'readState<BermSpan[]>("berm"',
|
|
"bermAt:",
|
|
):
|
|
assert word not in text, (rel, word)
|
|
shape = (ROOT / "common_util" / "common_util_structure_shape_keys.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert '"berm"' in shape
|
|
|
|
|
|
def test_시설_줄_종류_표_전_측점_안_칠함(out):
|
|
assert out["paint"]["before"] == 0 # 종류 표 전 = 빈 측점을 종단도에 안 씀
|
|
assert out["paint"]["after"] == 1 # 받은 뒤 한 번
|