feat(B06): 세월교 · 박스 흙막이 횡단 그림 — 관보호공 흙막이와 같은 벽 · 자리 · 연동 · 경사(73-4)
- 유입 · 유출 구조 「흙막이」면 배관 흙막이와 같은 세트(관 숨김 · body)를 같은 측점 culvert 자리에 함께 얹음(파이썬 _guard_set ↔ TS guardSet 짝 · 거울 시험) - 자동 자리 = 벽 전면이 관(세월교) · 본체(박스) 끝 · 좌우 x 는 그 자리 기준(본체를 늘리면 벽도 따라감) · 상하 d 는 배관과 같음 - 연동 옆 측점 · 3D 연동 해제 자리는 본체 기준 x 를 절대값으로 넘겨 받음 · 3D 만료 해시에 흙막이 값 포함 - 설계선 트림 = 벽 선 쪽은 흙막이 · 안 선 쪽은 본체 · 벽 선 쪽 본체 성토선은 걷음(두 줄 방지) · 관 연장은 안 실음 - 좌측 폼 → 캐시: 세트 다시 짓기 · 높이 · 형태 · 추가 흙막이 상자 수 = 유출 다단 수 - [연동][경사] 회색 풀림(옆 측점에 벽이 서면 연동 · 구간 있으면 경사) — 배관과 같은 규칙 - 시험: test_73_4_body_guard(벽 전면 = 본체 끝 · 박스 +1 m 따라감 · 연동 옆 측점 벽 · 서버 Node 면적 = 화면) · 거울 시험 흙막이 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,119 @@
|
||||
/* 73-4 세월교 · 박스 흙막이 횡단 기하 헬퍼 — 화면 TS(`B06_Section_Structure_Layouts` · `_Cross_Body_Guard`)를
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림. 입력 = 픽스처(검증_sub7 상세 일부) · 결과 = 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_73_4_body_guard.py`.
|
||||
*/
|
||||
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 STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", f);
|
||||
const { computeStoredLayouts, structureAreaRows, trimOfLayouts } = loadTs(
|
||||
B06("B06_Section_Structure_Layouts.ts"),
|
||||
);
|
||||
const { outletPipeEnd } = loadTs(B06("B06_Section_UI_Cross_Culvert_Tiers.ts"));
|
||||
const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts"));
|
||||
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
||||
);
|
||||
const sections = fixture.cross_sections;
|
||||
const at = (c) => sections.find((s) => Math.abs(s.chainage_m - c) < 0.6);
|
||||
const round = (v) => Math.round(v * 1000) / 1000;
|
||||
|
||||
/** 벽마다 전면(관 끝 자리) ↔ 본체 끝 차이(m) · 바닥 기준 ↔ 본체 끝 표고 차이. */
|
||||
function fronts(section) {
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
const base = bodyGuardBase(section, section.samples);
|
||||
const walls = layouts.culvert?.walls ?? [];
|
||||
return {
|
||||
walls: walls.map((w) => {
|
||||
const end = base.ends[w.role];
|
||||
const front = outletPipeEnd(w);
|
||||
return {
|
||||
role: w.role,
|
||||
form: w.form,
|
||||
dOffset: round(front.offset - end.offset),
|
||||
dElevation: round(front.elevation - end.elevation),
|
||||
front: round(front.offset),
|
||||
};
|
||||
}),
|
||||
bodyX: layouts.culvert?.bodyX ?? null,
|
||||
applied: layouts.culvert?.revetShift
|
||||
? { inlet: layouts.culvert.revetShift.inlet.x, outlet: layouts.culvert.revetShift.outlet.x }
|
||||
: null,
|
||||
trimSame:
|
||||
JSON.stringify(trimOfLayouts(layouts)) === JSON.stringify(layouts.culvert?.designTrim),
|
||||
pipeLength: null,
|
||||
};
|
||||
}
|
||||
|
||||
const out = {};
|
||||
const box = at(352.14);
|
||||
const ford = at(173.09);
|
||||
out.box = fronts(box);
|
||||
out.ford = fronts(ford);
|
||||
|
||||
// 박스를 좌우로 1 m 늘리면 벽 전면도 1 m 따라 나감(연동 = 본체 기준 자리)
|
||||
const moved = JSON.parse(JSON.stringify(box));
|
||||
moved.design.box_adjust = { left: { lengthM: 1, riseM: 0 }, right: { lengthM: 1, riseM: 0 } };
|
||||
const swapped = sections.map((s) => (s === box ? moved : s));
|
||||
const movedLayouts = computeStoredLayouts(moved, swapped);
|
||||
const base = bodyGuardBase(moved, moved.samples);
|
||||
out.boxMoved = movedLayouts.culvert.walls.map((w) => ({
|
||||
role: w.role,
|
||||
dOffset: round(outletPipeEnd(w).offset - base.ends[w.role].offset),
|
||||
front: round(outletPipeEnd(w).offset),
|
||||
}));
|
||||
|
||||
// 연동 옆 측점(340 · 360) — 소유 측점 벽이 섬
|
||||
out.linked = [340, 360].map((c) => {
|
||||
const s = at(c);
|
||||
return { chainage: c, walls: computeStoredLayouts(s, sections)?.culvert?.walls.length ?? 0 };
|
||||
});
|
||||
|
||||
// 면적 행 — 서버 Node 진입점과 같은 함수(`structureAreaRows`)
|
||||
out.areas = structureAreaRows(sections).filter((r) =>
|
||||
[173.087, 352.14].some((c) => Math.abs(r.chainage_m - c) < 0.6),
|
||||
);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,82 @@
|
||||
"""73-4 세월교 · 박스 흙막이 — 관보호공 흙막이와 같은 벽 · 같은 자리 규칙(관 · 본체 끝).
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_73_4_body_guard.cjs`)과 서버 Node 번들(면적)을 대조함.
|
||||
픽스처 = 검증_sub7 상세 일부(세월교 173.09 유입 · 유출 흙막이 · 박스 352.14 유입 · 유출 흙막이 전후 15 · 10 m).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
HELPER = Path(__file__).parent / "helper_73_4_body_guard.cjs"
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "b06_73_4_body_guard_sections.json"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def screen() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_박스_벽_전면이_본체_끝(screen: dict) -> None:
|
||||
walls = screen["box"]["walls"]
|
||||
assert {w["role"] for w in walls} == {"inlet", "outlet"}
|
||||
for wall in walls:
|
||||
assert wall["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
assert wall["dElevation"] == pytest.approx(0, abs=1e-3)
|
||||
# 형태 = 폼 값 그대로(유입 찰 · 유출 콘크리트)
|
||||
assert {w["role"]: w["form"] for w in walls} == {"inlet": "돌쌓기(찰)", "outlet": "콘크리트"}
|
||||
# 자동 자리 = 본체 기준 0 — 되받아 저장해도 본체를 따라감
|
||||
assert screen["box"]["applied"] == {"inlet": 0, "outlet": 0}
|
||||
|
||||
|
||||
def test_박스를_늘리면_벽도_m_단위로_따라감(screen: dict) -> None:
|
||||
before = {w["role"]: w["front"] for w in screen["box"]["walls"]}
|
||||
for wall in screen["boxMoved"]:
|
||||
assert wall["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
assert abs(wall["front"] - before[wall["role"]]) == pytest.approx(1.0, abs=1e-3)
|
||||
|
||||
|
||||
def test_세월교_벽_전면이_관_끝(screen: dict) -> None:
|
||||
walls = {w["role"]: w for w in screen["ford"]["walls"]}
|
||||
assert walls["outlet"]["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
assert walls["outlet"]["dElevation"] == pytest.approx(0, abs=1e-3)
|
||||
# 유입 2.5 m 벽은 관 끝 자리에선 노면 위로 솟음 — 배관 벽과 같이 상단이 노견에서 멈춰 내려앉음
|
||||
assert walls["inlet"]["dElevation"] <= 0
|
||||
|
||||
|
||||
def test_연동_옆_측점에도_벽이_섬(screen: dict) -> None:
|
||||
assert [row["walls"] for row in screen["linked"]] == [2, 2]
|
||||
|
||||
|
||||
def test_설계선_트림은_흙막이_쪽(screen: dict) -> None:
|
||||
assert screen["box"]["trimSame"] and screen["ford"]["trimSame"]
|
||||
|
||||
|
||||
def test_서버_면적이_화면과_같음(screen: dict) -> None:
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT, BUNDLE
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
sections = json.loads(FIXTURE.read_text(encoding="utf-8"))["cross_sections"]
|
||||
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, {"detail": {"cross_sections": sections}})
|
||||
assert output, "서버 Node 번들 실행 실패"
|
||||
server = {round(r["chainage_m"], 2): r for r in output["areas"]}
|
||||
for row in screen["areas"]:
|
||||
mine = server[round(row["chainage_m"], 2)]
|
||||
for key in ("cut_area_m2", "fill_area_m2"):
|
||||
assert mine[key] == pytest.approx(row[key], abs=1e-4), (row["chainage_m"], key)
|
||||
# 세월교 · 박스 흙막이 세트는 관이 아님 — 관 연장 안 실음
|
||||
assert all("pipe_length_m" not in r for r in screen["areas"])
|
||||
@@ -53,6 +53,10 @@ POINTS = [
|
||||
"wing_in_length_m": 3.0,
|
||||
"wing_in_angle_deg": 30.0,
|
||||
"wing_out": "없음",
|
||||
# 세월교 유입 흙막이(73-4)
|
||||
"inlet_type": "흙막이",
|
||||
"inlet_revet_form": "콘크리트",
|
||||
"inlet_revet_height_m": 1.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,6 +68,10 @@ POINTS = [
|
||||
"wing_in": "날개벽",
|
||||
"wing_out": "흙막이",
|
||||
"wing_out_length_m": 2.0,
|
||||
# 박스 유출 흙막이(73-4) — 배관 흙막이와 같은 칸 · `culvert` 자리에 함께 얹힘
|
||||
"outlet_revet_form": "돌쌓기(찰)",
|
||||
"outlet_revet_height_m": 3.5,
|
||||
"outlet_revet_length_m": 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -175,3 +183,19 @@ def test_culvert_sets_match_between_python_and_ts(tmp_path: Path) -> None:
|
||||
assert left == right, f"{CHAINAGES[index]}m 측점 세트가 갈렸다"
|
||||
# 시설이 실제로 붙었는지 — 빈 결과끼리 같아서 통과하는 것을 막는다.
|
||||
assert sum(1 for item in expected if len(item) > 1) >= 6
|
||||
|
||||
|
||||
def test_ford_box_guard_attached_as_culvert() -> None:
|
||||
"""세월교 · 박스 흙막이(73-4) — 흙막이 고른 쪽만 기슭막이 · 관 숨김 · 자리 기준 = 본체."""
|
||||
sections = {item["chainage_m"]: item for item in _python_sections()}
|
||||
ford, box = sections[60.0]["culvert"], sections[80.0]["culvert"]
|
||||
assert (ford["body"], ford["hidden_pipe"], ford["diameter_m"]) == ("ford", True, 1.0)
|
||||
assert ford["inlet"]["structure"] == "기슭막이" and ford["outlet"]["structure"] == ""
|
||||
assert ford["inlet"]["revet_form"] == "콘크리트"
|
||||
assert box["body"] == "box" and box["inlet"]["structure"] == ""
|
||||
assert box["outlet"]["revet_height_m"] == 3.5
|
||||
# 본체 전체 높이 = 상판 + 내공 2.5 + 저판
|
||||
b = sections[80.0]["box"]
|
||||
assert box["diameter_m"] == round(b["top_thickness_m"] + 2.5 + b["slab_thickness_m"], 3)
|
||||
# 흙막이 없는 시설엔 안 얹음
|
||||
assert "culvert" not in sections[100.0]
|
||||
|
||||
Reference in New Issue
Block a user