feat(b05,b06): 물넘이포장 콘크리트 윗면 = 설계 노면 · 포장층 노견까지 · 월류 높이 점선은 노면 위 (287-2 · 287-4)

- 설계 계산 tilted · tilt_pct(파이썬 · TS 짝) — 물넘이 범위 노면 = 경사각도로 지대 낮은 쪽 한 경사 · 측구 없음 · 중심점 = 계획고(콘크리트 윗면) · 노면 끝에서 사면이 이어짐(짧으면 성토)
- 서버 tilt_ranges · tilt_args · stored_tilt — 기본 설계 · [저장] 재계산 · 포장 범위 맞춤 · 저장분 재계산이 같은 값(옛 ford_paved 갈음)
- 횡단 그림 — 포장층 = 노면공 포장 형상(appendPavementOverlay)을 노견까지 · 물넘이 표식 · 월류 높이선 = 노면 + 높이 같은 각도 · 옛 파임 · 노면 걷기 그림 걷음
- 3D 노면 파기 걷음(설계 노면이 곧 윗면) · 코리도 빌드 판 94
- 시험 test_287 · 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:
2026-10-12 20:57:54 +09:00
co-authored by Claude Opus 5.5
parent 0c7f71d103
commit a5a9a7cec0
23 changed files with 273 additions and 288 deletions
@@ -25,23 +25,8 @@ const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_sta
const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"), {
"./B06_Section_UI_Cross_Wall_Hatch": { appendChuteBand() {} },
"./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} },
});
// 286-2 노면 기울기 — 설계선 양 끝 표고가 높은 쪽 = 유입 · 표고 낮은 쪽(유출)으로 기움
const SPEC = { span_m: 6, depth_m: 0.3, slope_pct: 5 };
const designOf = (left, right, mode) => ({
design_elevation_m: 100,
cross_slope_pct: 2,
section_mode: mode,
design_line: [
{ offset_m: -10, elevation_m: right },
{ offset_m: 10, elevation_m: left },
],
});
const deck = (design) => ({
left: ford.fordDeckElevationAt(design, SPEC, 3),
right: ford.fordDeckElevationAt(design, SPEC, -3),
});
// 286-3 도수로 · 돌붙임 길이 — 유출(좌 · 우가 높음) 성토사면 1:1.5(5 m 내려감 · 사면 길이 √(7.5²+5²) ≈ 9.01)
const sets = loadTs(path.join(ROOT, "common_util", "common_util_culvert_sets.ts"));
const slopeDesign = {
@@ -73,8 +58,5 @@ const out = {
lineTooLong: ford.fordChuteLine(slopeDesign, chuteOf({ ford_chute_1_length_m: 50 })),
fordMarks: station.pipeStationMarks({ chainage_m: 230, facility: "ford_pavement" }),
planted: station.plantedChainages(input.pipes, input.structures, input.types),
deckLeftHigh: deck(designOf(105, 95, "right_cut")),
deckRightHigh: deck(designOf(95, 105, "left_cut")),
deckFlat: deck(designOf(100, 100, "right_cut")),
};
process.stdout.write(JSON.stringify(out));
@@ -36,7 +36,65 @@ const change = (form, c, i) => {
return form.state().chute[0].area;
};
// 287-2 · 287-4 그림 — DOM 흉내(속성 · 자식만)
class Node {
constructor(tag) {
this.tag = tag;
this.attrs = {};
this.children = [];
this.dataset = {};
this.style = {};
}
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 pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
// 서버가 지대 낮은 쪽(우)으로 5 % 기울인 노면 · 노견 끝 ±3 · 차도 끝 ±2.5
const tilted = {
section_mode: "left_cut",
tilted: true,
cross_slope_pct: 5,
design_elevation_m: 100,
pavement_thickness_m: 0.2,
paved: true,
road_edges: {
left: { offset_m: 3, elevation_m: 100.15 },
right: { offset_m: -3, elevation_m: 99.85 },
},
carriageway_edges: {
left: { offset_m: 2.5, elevation_m: 100.125 },
right: { offset_m: -2.5, elevation_m: 99.875 },
},
design_line: [
{ offset_m: 6, elevation_m: 103 },
{ 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 drew = pave.appendFordPavementOverlay(
svg,
{ span_m: 8, depth_m: 0.4, slope_pct: 5 },
tilted,
(o) => o,
(e) => e,
);
const ofClass = (c) => svg.children.filter((k) => (k.attrs.class ?? "").split(" ").includes(c));
const out = {
drew,
pavePoints: ofClass("b06-chart__ford-pavement").map((k) => k.attrs.points),
planPoints: ofClass("b06-chart__ford-deck-plan").map((k) => k.attrs.points),
inflowLeft: pave.inflowLeft(tilted),
// 287-1 길이 넣음 = 월류 폭 × 길이 · 비움 = 기본값 · 폭 바꿈 = 길이 있을 때만 다시 셈 · 폭 비면 기본 폭
areaByLength: change(formOf({ width: "6" }, { length_m: "4", area: "30" }), "chute", "length_m"),
areaCleared: change(formOf({ width: "6" }, { length_m: "", area: "24" }), "chute", "length_m"),
@@ -103,14 +103,12 @@ def _ground(slope: float) -> list[dict]:
def test_물넘이_자리_측구_없음():
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
plain = compute_cross_design(_ground(0.35), 100.4, **kwargs)
ford = compute_cross_design(_ground(0.35), 100.4, ford_paved=True, **kwargs)
assert plain["ditch_enabled"] is True and "ford_paved" not in plain
assert ford["ditch_enabled"] is False and ford["ford_paved"] is True
ford = compute_cross_design(_ground(0.35), 100.4, tilted=True, **kwargs)
assert plain["ditch_enabled"] is True and "tilted" not in plain
assert ford["ditch_enabled"] is False and ford["tilted"] is True
assert ford["ditch_area_m2"] == 0
# 사용자가 측구를 켜도 물넘이 자리는 안 섬
forced = compute_cross_design(
_ground(0.35), 100.4, ford_paved=True, ditch_choice=True, **kwargs
)
forced = compute_cross_design(_ground(0.35), 100.4, tilted=True, ditch_choice=True, **kwargs)
assert forced["ditch_enabled"] is False
@@ -125,21 +123,22 @@ def test_물넘이_범위_서버_판정(tmp_path):
assert paved_at(230.0, ranges) and paved_at(226.0, ranges) and not paved_at(240.0, ranges)
def test_노면은_표고_낮은_쪽으로(ts):
# 좌 높음(입력 단면유형이 반대여도 지형) · 우 높음 · 같으면 단면유형(right_cut = 우 상류)
assert ts["deckLeftHigh"]["left"] > ts["deckLeftHigh"]["right"]
assert ts["deckRightHigh"]["right"] > ts["deckRightHigh"]["left"]
assert ts["deckFlat"]["right"] > ts["deckFlat"]["left"]
# 경사 5 % · 노선 중심 100 − 깊이 0.3 · 3 m 에서 ±0.15
assert ts["deckLeftHigh"]["left"] == pytest.approx(99.85)
assert ts["deckLeftHigh"]["right"] == pytest.approx(99.55)
def test_노면은_표고_낮은_쪽으로():
# 287-4 — 설계 노면 = 경사각도(%)로 지대 낮은 쪽 한 경사 · 중심 = 계획고(콘크리트 윗면)
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
for slope, high in ((0.35, "left"), (-0.35, "right")):
got = compute_cross_design(_ground(slope), 100.4, tilted=True, tilt_pct=5.0, **kwargs)
left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right"))
assert (left > right) == (high == "left")
assert got["design_elevation_m"] == 100.4 and got["cross_slope_pct"] == 5.0
assert got["tilt_pct"] == 5.0
def test_저장_뒤_서버가_물넘이_측구를_맞춤():
router = _read("B06_Section", "B06_Section_Router_Design.py")
assert 'bool(design.get("ford_paved")) == ford_paved' in router
assert "stored_tilt(design) == tilt" in router
refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts")
assert "fordPaved: Boolean(section.ford_pavement)," in refresh
assert "tilted: Boolean(section.ford_pavement)," in refresh
CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30}
@@ -3,6 +3,9 @@
못박는 것
- 287-1 도수로/돌붙임 길이 ↔ 면적 — 길이 넣음 = 월류 폭 × 길이 · 비움 = 칸 기본값 · 월류 폭을 바꾸면
길이가 있을 때만 다시 셈 · 폭이 비면 물넘이 기본 폭.
- 287-2 · 287-4 콘크리트 윗면 = 설계 노면(서버 · TS 짝이 경사각도로 지대 낮은 쪽 한 경사 · 중심점이 그 위)
· 포장층 = 노면공 포장 형상 노견까지(노면공 항목과 안 섞임) · 월류 높이 점선 = 노면 위 같은 각도 ·
3D 노면을 파지 않음 · 설계 측구 없음.
진짜 TS 는 `helper_287_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
"""
@@ -55,3 +58,26 @@ def test_도수로_길이_면적(ts):
def test_물넘이_폼이_길이_면적을_잇는다():
form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts")
assert "syncChuteArea(paveForm, c, item), paveFlowBox.onChange(c, item)" in form
def _pts(text: str) -> list[float]:
return [float(v) for pair in text.split() for v in pair.split(",")]
def test_포장층_노견까지_점선은_노면_위(ts):
assert ts["drew"] is True
# 포장층 하나 = 노견 끝(±3)까지 · 윗면 = 설계 노면 · 두께 0.2
assert len(ts["pavePoints"]) == 1
assert _pts(ts["pavePoints"][0]) == pytest.approx([3, 100.15, -3, 99.85, -3, 99.65, 3, 99.95])
# 월류 높이선 = 노면 + 0.4 · 같은 각도
assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, -3, 100.25])
assert ts["inflowLeft"] is True # 노면 높은 쪽 = 유입 · 도수로 띠 = 낮은 쪽
def test_3D_노면을_파지_않고_옛_파임_그림을_걷음():
station = _read("B05_Profile", "B05_Profile_UI_Corridor_Station.ts")
assert "fordDeckElevationAt" not in station
pave = _read("B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts")
assert "hideFordPavedRoad" not in pave and "fordDeckElevationAt" not in pave
view = _read("B06_Section", "B06_Section_UI_Cross_View.ts")
assert "hideFordPavedRoad" not in view
@@ -228,7 +228,21 @@ CASES = [
"section_mode": "left_cut",
"ditch_side": "left",
"paved": True,
"ford_paved": True,
"tilted": True,
},
},
{
# 287-4 — 물넘이포장 자리 노면 = 경사각도로 지대 낮은 쪽 한 경사(지반이 좌로 오름 = 우가 낮음)
"name": "물넘이포장 자리 노면 기울임",
"samples": _ground(0.35),
"design_elevation_m": 100.4,
"options": {
"ground_type": "soil",
"section_mode": "left_cut",
"ditch_side": "left",
"paved": True,
"tilted": True,
"tilt_pct": 5.2408,
},
},
]
@@ -254,7 +268,8 @@ const camel = {
curve_widening_m: "curveWideningM",
shoulder_extend_left_m: "shoulderExtendLeftM",
shoulder_extend_right_m: "shoulderExtendRightM",
ford_paved: "fordPaved",
tilted: "tilted",
tilt_pct: "tiltPct",
};
// 사토장 제원만 칸 이름이 다르다 — 파이썬은 snake, TS 는 camel 이다.
const spoilCamel = { side: "side", width_m: "widthM", slope_ratio_n: "slopeRatioN" };