feat(B06): 세월교 바닥 손 조정 칸 — 날개벽 칸에 측벽 높이 줄 · 손 값은 원지반 바뀌어도 그대로(91-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -64,11 +64,12 @@ const PIPE_INLET_ROWS = [
|
||||
".b06-structure-panel__struct--basin-size",
|
||||
].join(", ");
|
||||
const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";
|
||||
const WALL_HEIGHT_ROW = ".b06-structure-panel__struct--wall-height";
|
||||
|
||||
/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은 안 받음
|
||||
* (높이 · 측벽 좌우 · 상하 · 바닥판 읽기는 칸에 안 둠 · 강조만 · 65-1). */
|
||||
/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은
|
||||
* 세월교 측벽 높이(바닥 손 조정 · 91-2)만 받음(좌우 · 상하 · 바닥판 읽기는 칸에 안 둠 · 65-1). */
|
||||
function rowsFor(slot: HTMLElement): string | null {
|
||||
if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null;
|
||||
if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return WALL_HEIGHT_ROW;
|
||||
if (slot === sideSlots?.outlet) return PIPE_OUTLET_ROWS;
|
||||
return slot === sideSlots?.inlet ? PIPE_INLET_ROWS : SIDE_ROWS;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,9 @@ export function buildFordPanel(deps: FordPanelDeps, opts?: { dock?: boolean }):
|
||||
`높이 ${deps.heightFor(current).toFixed(2)}m` + (adjust.heightM === null ? " (자동)" : "");
|
||||
moveLine.textContent = `좌우 ${adjust.lateralM.toFixed(1)}m · 상하 ${adjust.slopeM.toFixed(1)}m`;
|
||||
slabLine.textContent = `바닥판 ${deps.slabLengthM().toFixed(2)}m`;
|
||||
heightValue.textContent = `${deps.heightFor(current).toFixed(1)}m`;
|
||||
// 자동 = 바닥판이 계류 하상을 따라감 · 손 값 = 원지반이 바뀌어도 그대로(91-2)
|
||||
heightValue.textContent =
|
||||
`${deps.heightFor(current).toFixed(1)}m` + (adjust.heightM === null ? " 자동" : "");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* 91 원지반 · 암 제약 삭제 · 유출측 복합경사 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림.
|
||||
* 입력 = 73-4 픽스처(세월교 173.09 · 박스 352.14 · 관 180) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_91_ground_free.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 } = loadTs(B06("B06_Section_Structure_Layouts.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 clone = (v) => JSON.parse(JSON.stringify(v));
|
||||
const r3 = (v) => Math.round(v * 1000) / 1000;
|
||||
const out = {};
|
||||
/** 원지반 사본 — [from, to] 안 샘플을 dz 만큼 옮김. */
|
||||
const shiftGround = (s, dz, from = -Infinity, to = Infinity) => {
|
||||
for (const p of s.samples)
|
||||
if (p.offset_m >= from && p.offset_m <= to && p.elevation_m != null) p.elevation_m += dz;
|
||||
};
|
||||
const layoutsOf = (s, base) =>
|
||||
computeStoredLayouts(
|
||||
s,
|
||||
sections.map((x) => (x === base ? s : x)),
|
||||
);
|
||||
|
||||
// ① 91-2 세월교 바닥 — 자동은 하상을 따라감 · 손 값(측벽 높이)은 원지반을 바꿔도 그대로 · 흙막이 기본 높이도
|
||||
const FORD = at(173.087);
|
||||
function fordFloor(edit) {
|
||||
const s = clone(FORD);
|
||||
for (const role of ["inlet", "outlet"]) s.culvert[role].revet_height_m = null;
|
||||
edit(s);
|
||||
const { ford, culvert } = layoutsOf(s, FORD);
|
||||
return ford.sides.map((side) => {
|
||||
const wall = culvert.walls.find((w) => w.role === side.role);
|
||||
return {
|
||||
role: side.role,
|
||||
slabTop: r3(side.slabTopElevation),
|
||||
wallBottom: wall ? r3(Math.min(...wall.points.map((p) => p.elevation))) : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
const hand = (s) => {
|
||||
s.design.ford_adjust = {
|
||||
inlet: { heightM: 2.0, lateralM: 0, slopeM: 0 },
|
||||
outlet: { heightM: 2.4, lateralM: 0, slopeM: 0 },
|
||||
};
|
||||
};
|
||||
out.fordAuto = fordFloor(() => undefined);
|
||||
out.fordAutoBedDown = fordFloor((s) => shiftGround(s, -2, -2, 2));
|
||||
out.fordHand = fordFloor(hand);
|
||||
out.fordHandBedDown = fordFloor((s) => {
|
||||
hand(s);
|
||||
shiftGround(s, -2, -2, 2);
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -147,8 +147,12 @@ def test_집계표_세월교_흙막이_상자_건():
|
||||
]
|
||||
|
||||
|
||||
def test_B06_측벽_읽기_줄은_세월교_박스_칸에_안_옮김():
|
||||
def test_B06_세월교_박스_칸은_측벽_높이_줄만_받음():
|
||||
dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8")
|
||||
assert "slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null" in dock
|
||||
assert (
|
||||
"slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return WALL_HEIGHT_ROW"
|
||||
in dock
|
||||
)
|
||||
assert 'WALL_HEIGHT_ROW = ".b06-structure-panel__struct--wall-height"' in dock
|
||||
lists = (ROOT / "B05_Profile/B05_Profile_UI_Structures_List.ts").read_text(encoding="utf-8")
|
||||
assert "return isNone(c, state[c.id][0])" in lists
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""91 원지반 · 암 제약 삭제 · 유출측 복합경사.
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_91_ground_free.cjs`) · 픽스처 = 73-4 세월교 173.09 · 박스 352.14 · 관 180.
|
||||
"""
|
||||
|
||||
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_91_ground_free.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def fit() -> 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_세월교_바닥_자동은_하상을_따라감(fit: dict) -> None:
|
||||
for before, after in zip(fit["fordAuto"], fit["fordAutoBedDown"]):
|
||||
assert after["slabTop"] < before["slabTop"] - 1.0
|
||||
|
||||
|
||||
def test_세월교_바닥_손_값은_원지반을_바꿔도_그대로(fit: dict) -> None:
|
||||
assert fit["fordHand"] == fit["fordHandBedDown"]
|
||||
# 손 값 = 측벽 높이 · 흙막이 바닥 = 바닥판 밑(두께 0.3)
|
||||
for side in fit["fordHand"]:
|
||||
assert side["wallBottom"] == pytest.approx(side["slabTop"] - 0.3, abs=1e-3)
|
||||
|
||||
|
||||
def test_세월교_박스_칸은_측벽_높이_줄을_받음() -> None:
|
||||
dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8")
|
||||
assert "sideSlots?.wingOutlet) return WALL_HEIGHT_ROW" in dock
|
||||
Reference in New Issue
Block a user