fix(B06 횡단): 사면 이동 절 ↔ 성 판정을 옮긴 노견에서 — 마루 너머 = 성토 1:n (370-1)

- 짝 common_util_cross_design_geometry.ts ↔ B06_Section_Engine_Design_Geometry.py — 노견 확장을 tilt 바로 뒤로 · 단면유형 · 자동 측구 판정은 옮긴 뒤
- 거울 사례 「마루 너머 = 성토 1:n」 · 「성 → 절 넘어감」 · 옛 사례 이름 「판정은 옮긴 노견」
- 서버 묶음 셋 다시 지음(이 파일 안 실음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
2026-10-17 11:47:28 +09:00
co-authored by Claude Opus 5.5
parent cd27e7d367
commit 297910d28c
3 changed files with 92 additions and 21 deletions
@@ -103,6 +103,16 @@ class _SectionGeometry:
low_left = ground_at(self.left_extent) < ground_at(-self.right_extent)
self.slope_per_offset = -slope if low_left else slope
# 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 노견(폭 고정) · 측구 · 사면이
# 같이 바깥으로 · 늘어난 폭은 차도 · 단면유형 · 측구 판정은 **옮긴 뒤** 노견에서(아래 ·
# 370-1 — 마루를 넘으면 그 쪽 사면 기울기로 원지반까지). ⚠ TS 짝 `SectionGeometry`.
self.shoulder_extend_left = max(shoulder_extend_left_m, 0.0)
self.shoulder_extend_right = max(shoulder_extend_right_m, 0.0)
self.half_road_left += self.shoulder_extend_left
self.half_road_right += self.shoulder_extend_right
self.left_extent += self.shoulder_extend_left
self.right_extent += self.shoulder_extend_right
# 단면 유형 자동 판정(D-2): 각 측 절/성토 역할을 노면 끝 지반이 설계면보다
# 높은지(절토)/낮은지(성토)로 결정한다. 좌절/우절/양절/양성이 모두 지형에서
# 자연 도출된다. 사용자 입력 section_mode는 측구 방향(ditch_side) 기본값에만 쓰고
@@ -156,16 +166,6 @@ class _SectionGeometry:
if tilted:
self.has_ditch = False
# 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
# 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도.
# ⚠ TS 짝 `SectionGeometry`.
self.shoulder_extend_left = max(shoulder_extend_left_m, 0.0)
self.shoulder_extend_right = max(shoulder_extend_right_m, 0.0)
self.half_road_left += self.shoulder_extend_left
self.half_road_right += self.shoulder_extend_right
self.left_extent += self.shoulder_extend_left
self.right_extent += self.shoulder_extend_right
# 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용).
self.ditch_points: list[tuple[float, float]] = []
edge_offset = self.left_extent if ditch_side == "left" else -self.right_extent
@@ -180,6 +180,16 @@ export class SectionGeometry {
this.slopePerOffset = lowLeft ? -slope : slope;
}
// 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 노견(폭 고정) · 측구 · 사면이 같이
// 바깥으로 · 늘어난 폭은 차도 · 단면유형 · 측구 판정은 **옮긴 뒤** 노견에서(아래 · 370-1 — 마루를
// 넘으면 그 쪽 사면 기울기로 원지반까지). ⚠ 파이썬 짝 `_SectionGeometry`.
this.shoulderExtendLeft = Math.max(params.shoulderExtendLeftM ?? 0, 0);
this.shoulderExtendRight = Math.max(params.shoulderExtendRightM ?? 0, 0);
this.halfRoadLeft += this.shoulderExtendLeft;
this.halfRoadRight += this.shoulderExtendRight;
this.leftExtent += this.shoulderExtendLeft;
this.rightExtent += this.shoulderExtendRight;
// 단면유형 자동 판정(D-2) — 노면 끝 지반이 설계면보다 높으면 절토, 낮으면 성토.
const groundAt = params.groundAt;
if (groundAt !== null) {
@@ -226,15 +236,6 @@ export class SectionGeometry {
// 물넘이포장 · 맹암거 자리(286-2 · 288-1) — 측구가 안 섬 · 선택은 그대로(파이썬 짝)
if (params.tilted) this.hasDitch = false;
// 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
// 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`.
this.shoulderExtendLeft = Math.max(params.shoulderExtendLeftM ?? 0, 0);
this.shoulderExtendRight = Math.max(params.shoulderExtendRightM ?? 0, 0);
this.halfRoadLeft += this.shoulderExtendLeft;
this.halfRoadRight += this.shoulderExtendRight;
this.leftExtent += this.shoulderExtendLeft;
this.rightExtent += this.shoulderExtendRight;
// 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용).
const edgeOffset = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent;
const outward = params.ditchSide === "left" ? 1 : -1;
@@ -61,6 +61,22 @@ def _dip_ground() -> list[dict]:
return samples
def _ridge_ground() -> list[dict]:
"""우측 −4 m 까지 노면보다 높은 평지(절토) · 그 너머 급히 내려가는 마루(370-1)."""
return [
{"offset_m": o, "elevation_m": round(100.3 - 0.6 * max(0.0, -o - 4.0), 4), "valid": True}
for o in (-12.0 + i * 0.5 for i in range(49))
]
def _rise_ground() -> list[dict]:
"""좌측 +3 m 까지 노면보다 낮은 평지(성토) · 그 너머 오르막(370-1)."""
return [
{"offset_m": o, "elevation_m": round(99.6 + 0.4 * max(0.0, o - 3.0), 4), "valid": True}
for o in (-12.0 + i * 0.5 for i in range(49))
]
# 갈래를 골고루 태운다 — 지반유형 셋 · 단면유형 · 측구 형식 · 포장 · 2단 경사 · 세월교
# 노면 하강 · 곡선부 확폭(바깥쪽 좌/우) · 지반선 결측 구간.
CASES = [
@@ -214,8 +230,8 @@ CASES = [
},
},
{
# 노견 끝 근처에서 지반이 설계면을 넘는 자리 — 단면유형 · 측구 판정은 옮기기 전 노견에서
"name": "노견 확장 — 판정은 옮기기 전 노견",
# 노견 끝 근처에서 지반이 설계면을 넘는 자리 — 단면유형 · 측구 판정은 옮긴 노견에서(370-1)
"name": "노견 확장 — 판정은 옮긴 노견",
"samples": _ground(-0.05),
"design_elevation_m": 100.0,
"options": {
@@ -226,6 +242,30 @@ CASES = [
"shoulder_extend_left_m": 2.5,
},
},
{
# 370-1 — 우 절토 마루를 넘겨 옮김 = 그 쪽 성토 1:n 으로 원지반까지(원지반 직선 아님)
"name": "마루 너머 = 성토 1:n",
"samples": _ridge_ground(),
"design_elevation_m": 100.0,
"options": {
"ground_type": "soil",
"section_mode": "right_cut",
"ditch_side": "right",
"shoulder_extend_right_m": 3.0,
},
},
{
# 370-1 — 좌 성토를 오르막으로 옮김 = 그 쪽 절토(측구 자동 판정 따름)
"name": "성 → 절 넘어감",
"samples": _rise_ground(),
"design_elevation_m": 100.0,
"options": {
"ground_type": "soil",
"section_mode": "left_cut",
"ditch_side": "left",
"shoulder_extend_left_m": 2.5,
},
},
{
# 151-2 — 곡선부 확폭과 사면 이동이 같은 쪽에 겹침(확폭 출력엔 이동 몫 안 섞임)
"name": "확폭 좌 + 사면 이동 좌 0.4",
@@ -490,3 +530,33 @@ def test_사면_이동은_노견째_평행이동_늘어난_폭은_차도() -> No
both = by_name["확폭 좌 + 사면 이동 좌 0.4"]
assert both["widening_left_m"] == curve["widening_left_m"] > 0
assert abs(both["carriageway_width_m"] - (curve["carriageway_width_m"] + 0.4)) < 1e-9
def _outer_slope(result: dict, side: str, skip: float = 0.0) -> float:
"""노면 끝에서 `skip`(측구 폭) 바깥 설계선 한 마디의 기울기(dz / 바깥 거리)."""
sign = 1.0 if side == "left" else -1.0
start_offset = result["road_edges"][side]["offset_m"] + sign * skip
line = sorted(result["design_line"], key=lambda p: sign * p["offset_m"])
index = next(i for i, p in enumerate(line) if abs(p["offset_m"] - start_offset) < 1e-6)
start, nxt = line[index], line[index + 1]
return (nxt["elevation_m"] - start["elevation_m"]) / abs(nxt["offset_m"] - start["offset_m"])
def test_절성_넘어가면_그_쪽_사면_기울기로_원지반까지() -> None:
"""370-1 — 옮긴 노견에서 절/성 판정 · 마루 너머 = 성토 1:n · 성 → 절 = 절토 1:n + 측구 자동."""
by_name = dict(zip((case["name"] for case in CASES), _python_results(), strict=True))
soil = STANDARD_CROSS_SECTION["soil"]
ridge = by_name["마루 너머 = 성토 1:n"]
assert ridge["section_mode"] == "left_cut" # 우 = 성토
assert ridge["ditch_enabled"] is False # 우 측구 자동 판정 따라 빠짐
assert ridge["fill_area_m2"] > 0
assert abs(_outer_slope(ridge, "right") + 1.0 / soil["fill_slope_ratio"]) < 1e-3
rise = by_name["성 → 절 넘어감"]
assert rise["section_mode"] == "left_cut" # 좌 = 절토
assert rise["ditch_enabled"] is True and rise["ditch_side"] == "left"
assert (
abs(
_outer_slope(rise, "left", soil["ditch"]["top_width_m"]) - 1.0 / soil["cut_slope_ratio"]
)
< 1e-3
)