feat(B06): 유출측 절토 복합경사 공용 도우미 — 흙막이 수평공간 뒤 · 박스 끝 · 세월교 측벽 끝 · ㄴ·ㄷ 집수정 끝 · 절토 쪽 배관 유출 벽 뒤 · 절토선 점열 · 3D(91-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -127,4 +127,65 @@ out.lean = ["left_cut", "right_cut", "both_fill", "both_cut"].map((mode) => [
|
||||
leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌" }),
|
||||
leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌", face_role: "절토" }),
|
||||
]);
|
||||
// ③ 91-4 유출측 절토 복합경사 — 꼭짓점 사이 경사(수평/수직) · 사면 길이 · 끝 = 원지반 교점
|
||||
const { groundInterpolator } = loadTs(B06("B06_Section_UI_Cross_Culvert_Solve.ts"));
|
||||
function profile(points, samples) {
|
||||
const groundAt = groundInterpolator(samples);
|
||||
const legs = points.slice(1).map((p, i) => {
|
||||
const dx = Math.abs(p.offset - points[i].offset);
|
||||
const dz = p.elevation - points[i].elevation;
|
||||
return { ratio: dz > 1e-9 ? r3(dx / dz) : null, slant: r3(Math.hypot(dx, dz)), run: r3(dx) };
|
||||
});
|
||||
const end = points[points.length - 1];
|
||||
return { legs, endGap: r3(Math.abs(groundAt(end.offset) - end.elevation)) };
|
||||
}
|
||||
const withBerm = (s) => {
|
||||
s.design.berm = { width_m: 0.5, interval_m: 3, slope_deg: 0 };
|
||||
};
|
||||
// 흙막이 수평공간 뒤(박스 352.14 · 세월교 173.09)
|
||||
function guardTails(base, edit = () => undefined) {
|
||||
const s = clone(base);
|
||||
edit(s);
|
||||
const { culvert } = layoutsOf(s, base);
|
||||
return [...culvert.outletFill.segments, ...culvert.basinFill.segments]
|
||||
.filter((g) => g.kind === "cut")
|
||||
.map((g) => profile(g.points, s.samples));
|
||||
}
|
||||
out.guardTails = [...guardTails(at(352.14)), ...guardTails(FORD)];
|
||||
// 박스 끝 · 세월교 측벽 끝(흙막이 없음)
|
||||
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
|
||||
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford_Geom.ts"));
|
||||
function endCuts(base, compute) {
|
||||
const s = clone(base);
|
||||
delete s.culvert;
|
||||
s.uphill_side = "left"; // 절토 쪽(우측)을 유출로
|
||||
return compute(s, s.samples)
|
||||
.sides.filter((side) => side.cutLine)
|
||||
.map((side) => profile(side.cutLine, s.samples));
|
||||
}
|
||||
out.boxEnd = endCuts(at(352.14), computeBoxLayout);
|
||||
out.fordEnd = endCuts(FORD, computeFordLayout);
|
||||
// 절토 쪽 배관 유출 벽 뒤(관 340 · 유출 = 우측 절토) · 소단 있음/없음
|
||||
function cutOutlet(edit = () => undefined) {
|
||||
const s = clone(at(340));
|
||||
s.uphill_side = "left";
|
||||
s.culvert = {
|
||||
type: "pipe",
|
||||
chainage_m: 340,
|
||||
pipe_kind: "흄관",
|
||||
diameter_m: 1.0,
|
||||
min_cover_m: 0.5,
|
||||
inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
||||
outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
||||
};
|
||||
s.design.inlet_structure = "revet";
|
||||
edit(s);
|
||||
const layout = computeCulvertLayout(s, s.samples, undefined, "revet");
|
||||
const wall = layout.walls.find((w) => w.role === "outlet");
|
||||
const pts = layout.designTrim.minSlope.points;
|
||||
const from = pts.findIndex((p) => Math.abs(p.offset - wall.points[2].offset) < 1e-9);
|
||||
return profile(pts.slice(from), s.samples);
|
||||
}
|
||||
out.cutOutlet = cutOutlet();
|
||||
out.cutOutletBerm = cutOutlet(withBerm);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -80,7 +80,8 @@ def test_흙막이_끝_절토는_수평공간_뒤(shape: dict) -> None:
|
||||
tails = [tail for group in shape["cutDefault"] for tail in group]
|
||||
assert tails, "절토 끝선이 없음"
|
||||
for tail in tails:
|
||||
assert tail["points"] == 3
|
||||
# 시작 · 수평공간 끝 · (암 무릎 · 소단) · 원지반 교점 — 본선 절토와 같은 복합경사(91-4)
|
||||
assert tail["points"] >= 3
|
||||
assert tail["flat"] == pytest.approx(0, abs=1e-6)
|
||||
assert tail["berm"] == pytest.approx(1.0, abs=1e-3)
|
||||
for tail in shape["cutBerm2"]:
|
||||
|
||||
@@ -83,3 +83,34 @@ def test_지형에_막힘_잔재_없음() -> None:
|
||||
source = (ROOT / "B06_Section" / name).read_text(encoding="utf-8")
|
||||
assert "shiftBlockedM" not in source and "지형에 막힘" not in source, name
|
||||
assert "noticeExtraLimit" not in source and "noticeShortfall" not in source, name
|
||||
|
||||
|
||||
def _knee(cut: dict) -> bool:
|
||||
"""암(1:0.4) 뒤 토사(1:1.0) 무릎이 있음 — 픽스처 = 리핑암 · 2단계 · 경계 −0.5 m."""
|
||||
ratios = [leg["ratio"] for leg in cut["legs"] if leg["ratio"] is not None]
|
||||
return any(a == 0.4 and b == 1.0 for a, b in zip(ratios, ratios[1:]))
|
||||
|
||||
|
||||
def test_흙막이_수평공간_뒤_절토_복합경사(fit: dict) -> None:
|
||||
tails = fit["guardTails"]
|
||||
assert tails and all(cut["endGap"] < 0.01 for cut in tails)
|
||||
assert all(cut["legs"][0]["ratio"] is None for cut in tails) # 수평공간 먼저
|
||||
assert sum(_knee(cut) for cut in tails) >= 2
|
||||
|
||||
|
||||
def test_박스_끝_세월교_측벽_끝_절토_복합경사(fit: dict) -> None:
|
||||
for cuts in (fit["boxEnd"], fit["fordEnd"]):
|
||||
assert cuts and all(_knee(cut) and cut["endGap"] < 0.01 for cut in cuts)
|
||||
|
||||
|
||||
def test_절토_쪽_배관_유출_벽_뒤_절토사면_살아남(fit: dict) -> None:
|
||||
cut = fit["cutOutlet"]
|
||||
assert _knee(cut) and cut["endGap"] < 0.01
|
||||
|
||||
|
||||
def test_소단은_구조물_끝에서_새로_셈(fit: dict) -> None:
|
||||
legs = fit["cutOutletBerm"]["legs"]
|
||||
flat = next(i for i, leg in enumerate(legs) if leg["ratio"] is None)
|
||||
assert sum(leg["slant"] for leg in legs[:flat]) == pytest.approx(3.0, abs=1e-3)
|
||||
assert legs[flat]["run"] == pytest.approx(0.5, abs=1e-3)
|
||||
assert fit["cutOutletBerm"]["endGap"] < 0.01
|
||||
|
||||
Reference in New Issue
Block a user