feat(b06,b07): 맹암거 측점 횡단 — 측구 없음 · 암거 경사로 지대 낮은 쪽 기울기 · 노면 아래 암거 높이 돌 무늬 형상 (288-1)
- 서버 B06_Section_Engine_Tilt — 기울기 자리(물넘이 범위 + 맹암거 기준 측점 · 경사 = 경사각도 · 암거 경사 · 비면 표준 횡단경사) · 맹암거 제원 얹기(attach_underdrains) · 상세 · 재생성 응답에 얹음 - TS 짝 common_util_structure_underdrain(초안 구조물로 다시 얹음) · 계획선 편집 재계산도 맹암거 자리 기울임 - 형상(B06_Section_UI_Cross_Underdrain) — 노면을 높이만큼 아래로 옵셋 · 성토 쪽 = 성토 사면과 만남 · 절토 쪽 = 절토 사면을 늘여 만남 · 안 = 돌 무늬 · 높이 비면 안 그림 · B06 카드 · B07 같은 그림 - tilt_ranges · tilt_args 는 Router_Design 에서 새 모듈로 옮김(700줄) - 시험 test_288 새로 · 286_1 문자열 대조 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/* 288 맹암거 횡단 헬퍼 — 진짜 TS 를 Node 로 돌려 표준출력 JSON 한 벌로.
|
||||
* 입력 = argv[2] JSON 파일 { structures, sections }.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`));
|
||||
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
|
||||
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
|
||||
return new Proxy({}, { get: () => () => {} });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
class Node {
|
||||
constructor(tag) {
|
||||
this.tag = tag;
|
||||
this.attrs = {};
|
||||
this.children = [];
|
||||
}
|
||||
setAttribute(k, v) {
|
||||
this.attrs[k] = String(v);
|
||||
}
|
||||
getAttribute(k) {
|
||||
return this.attrs[k] ?? null;
|
||||
}
|
||||
append(...kids) {
|
||||
this.children.push(...kids);
|
||||
}
|
||||
}
|
||||
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const util = load(path.join(ROOT, "common_util", "common_util_structure_underdrain.ts"));
|
||||
const draw = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Underdrain.ts"));
|
||||
|
||||
const specs = util.underdrainSpecsFrom(input.structures);
|
||||
const sections = input.sections.map((s) => ({ ...s }));
|
||||
const attached = util.attachUnderdrainSpecs(sections, specs);
|
||||
|
||||
// 지대 낮은 쪽(우)으로 5 % 기울인 노면 · 좌 = 절토(오름) · 우 = 성토(내림) · 암거 높이 1 m
|
||||
const design = {
|
||||
road_edges: {
|
||||
left: { offset_m: 3, elevation_m: 100.15 },
|
||||
right: { offset_m: -3, elevation_m: 99.85 },
|
||||
},
|
||||
design_line: [
|
||||
{ offset_m: 6, elevation_m: 102.9 },
|
||||
{ offset_m: 3, elevation_m: 100.15 },
|
||||
{ offset_m: -3, elevation_m: 99.85 },
|
||||
{ offset_m: -7, elevation_m: 97.2 },
|
||||
],
|
||||
};
|
||||
const svg = new Node("g");
|
||||
const group = draw.appendUnderdrainOverlay(
|
||||
svg,
|
||||
{ structure_id: "ud-1", chainage_m: 40, height_m: 1, slope_pct: 5 },
|
||||
design,
|
||||
(o) => o,
|
||||
(e) => e,
|
||||
);
|
||||
const body = group?.children.find((k) => k.attrs.class === "b06-chart__underdrain-body");
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
specs,
|
||||
attached,
|
||||
sections: sections.map((s) => s.underdrain ?? null),
|
||||
outline: draw.underdrainOutline(design, 1),
|
||||
noHeight: draw.underdrainOutline(design, null),
|
||||
drew: !!group,
|
||||
bodyId: body?.attrs["data-structure-id"] ?? null,
|
||||
stones: group?.children.filter((k) => k.tag === "g").flatMap((g) => g.children).length ?? 0,
|
||||
}),
|
||||
);
|
||||
@@ -138,7 +138,7 @@ def test_저장_뒤_서버가_물넘이_측구를_맞춤():
|
||||
router = _read("B06_Section", "B06_Section_Router_Design.py")
|
||||
assert "stored_tilt(design) == tilt" in router
|
||||
refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts")
|
||||
assert "tilted: Boolean(section.ford_pavement)," in refresh
|
||||
assert "tilted: Boolean(section.ford_pavement || section.underdrain)," in refresh
|
||||
|
||||
|
||||
CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""288 맹암거 횡단 형상(요구 183-3 · 9).
|
||||
|
||||
못박는 것
|
||||
- 288-1 맹암거 기준 측점 = 측구 없음 · 노면 = 암거 경사로 지대 낮은 쪽 한 경사(물넘이 286 · 287 길 —
|
||||
서버 `tilt_ranges` · `tilt_args` · TS 짝 `tilted`) · 제원 얹기 서버 `attach_underdrains` ↔ TS
|
||||
`attachUnderdrainSpecs` 같은 값 · 형상 = 노면을 암거 높이만큼 아래로 옵셋 → 성토 쪽은 성토 사면과
|
||||
만남 · 절토 쪽은 절토 사면을 늘여 만남 · 그 안 돌 무늬 · 높이 비면 안 그림.
|
||||
|
||||
진짜 TS 는 `helper_288_underdrain.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))
|
||||
|
||||
import B06_Section.B06_Section_Engine_Tilt as tilt # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
|
||||
STRUCTURES = [
|
||||
{
|
||||
"structure_id": "ud-1",
|
||||
"type_id": "cross_underdrain",
|
||||
"placement": "point",
|
||||
"chainage_m": 40.0,
|
||||
"options": {"culvert_height_m": 0.8, "culvert_slope_pct": 4},
|
||||
},
|
||||
{ # 경사 · 높이 빈 맹암거 — 표준 횡단경사 · 형상 없음
|
||||
"structure_id": "ud-2",
|
||||
"type_id": "cross_underdrain",
|
||||
"placement": "point",
|
||||
"chainage_m": 80.0,
|
||||
"options": {},
|
||||
},
|
||||
{
|
||||
"structure_id": "x-1",
|
||||
"type_id": "cross_drain_exposed",
|
||||
"placement": "point",
|
||||
"chainage_m": 60.0,
|
||||
},
|
||||
]
|
||||
SECTIONS = [{"chainage_m": c} for c in (20.0, 40.0, 60.0, 80.0)]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def project(monkeypatch, tmp_path) -> Path:
|
||||
items = [StructureInstance(**s) for s in STRUCTURES]
|
||||
monkeypatch.setattr(tilt, "load_structures", lambda _root: (1, items))
|
||||
monkeypatch.setattr(tilt, "load_culvert_sets", lambda _root: {})
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
source = tmp_path_factory.mktemp("s288") / "in.json"
|
||||
source.write_text(
|
||||
json.dumps({"structures": STRUCTURES, "sections": SECTIONS}), encoding="utf-8"
|
||||
)
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_288_underdrain.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_맹암거_자리_기울기_서버(project):
|
||||
ranges = tilt.tilt_ranges(project)
|
||||
assert ranges == [(40.0, 40.0, 4.0), (80.0, 80.0, None)]
|
||||
assert tilt.tilt_args(40.0, ranges) == {"tilted": True, "tilt_pct": 4.0}
|
||||
assert tilt.tilt_args(80.01, ranges) == {"tilted": True, "tilt_pct": None}
|
||||
assert tilt.tilt_args(60.0, ranges) == {"tilted": False, "tilt_pct": None}
|
||||
|
||||
|
||||
def test_제원_얹기_서버_TS_같은_값(project, ts):
|
||||
sections = [dict(s) for s in SECTIONS]
|
||||
assert tilt.attach_underdrains(project, sections) == 2 == ts["attached"]
|
||||
assert [s.get("underdrain") for s in sections] == ts["sections"]
|
||||
assert ts["sections"][1] == {
|
||||
"structure_id": "ud-1",
|
||||
"chainage_m": 40.0,
|
||||
"height_m": 0.8,
|
||||
"slope_pct": 4.0,
|
||||
}
|
||||
|
||||
|
||||
def test_맹암거_측점_설계_측구_없음_낮은_쪽_기울기():
|
||||
samples = [
|
||||
{"offset_m": o / 2, "elevation_m": round(100.0 + 0.35 * o / 2, 4), "valid": True}
|
||||
for o in range(-24, 25)
|
||||
]
|
||||
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
|
||||
got = compute_cross_design(samples, 100.4, tilted=True, tilt_pct=4.0, **kwargs)
|
||||
assert got["ditch_enabled"] is False and got["cross_slope_pct"] == 4.0
|
||||
left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right"))
|
||||
assert left > right # 지반이 좌로 오름 = 우가 낮음
|
||||
|
||||
|
||||
def test_형상_옵셋선_성토_절토와_만남(ts):
|
||||
outline = [(p["offset"], p["elevation"]) for p in ts["outline"]]
|
||||
expected = [
|
||||
(3, 100.15), # 좌 노면 끝
|
||||
(-3, 99.85), # 우 노면 끝
|
||||
(-4.632653, 98.768367), # 우 성토 사면 ∩ 옵셋선
|
||||
(1.846154, 99.092308), # 좌 절토 사면을 늘인 선 ∩ 옵셋선
|
||||
]
|
||||
assert len(outline) == len(expected)
|
||||
for got, want in zip(outline, expected):
|
||||
assert got == pytest.approx(want, abs=1e-5)
|
||||
assert ts["noHeight"] is None
|
||||
assert ts["drew"] is True and ts["stones"] > 0
|
||||
|
||||
|
||||
def test_횡단도_B07_이_맹암거를_그림():
|
||||
view = (ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts").read_text(encoding="utf-8")
|
||||
assert (
|
||||
"appendUnderdrainOverlay(plotLayer, section.underdrain, section.design, x, toDisplayY);"
|
||||
in view
|
||||
)
|
||||
b07 = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_Cross_Overlay.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "appendUnderdrainOverlay(svg, section.underdrain, design, x, y);" in b07
|
||||
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
|
||||
assert "tilted: Boolean(section.ford_pavement || section.underdrain)," in refresh
|
||||
Reference in New Issue
Block a user