feat(b06): 물넘이 낮은 쪽 노면 끝이 절토면 콘크리트 · 노면선을 노면 기울기로 늘여 원지반까지 (294-3)

- 설계 계산(파이썬 _SectionGeometry · TS 짝 SectionGeometry) — 기울인 자리(tilted) 노면이 내려가는 쪽 노면 끝 원지반이 노면보다 높으면 같은 기울기로 늘여 원지반과 처음 만나는 자리까지 · 그 쪽 절토 사면 없음 · 샘플 반폭 안에서 못 만나면 옛 길 · 성토 쪽 그대로 · 결과 tilt_reach
- 토공 면적 · 설계선이 늘인 노면을 따름(서버 정본 · [저장] 재계산 · 거울 시험 갈래)
- 횡단 그림 — 콘크리트(포장층) · 월류 높이선이 늘인 끝까지(fordEdges)
- 시험 test_294_3 새로 · 횡단 설계 거울 「물넘이포장 낮은 쪽 노면 연장」

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 21:47:54 +09:00
co-authored by Claude Opus 5.5
parent b87c55c846
commit 7b3bb72b54
9 changed files with 306 additions and 1 deletions
@@ -63,6 +63,7 @@ class _SectionGeometry:
shoulder_extend_left_m: float = 0.0,
shoulder_extend_right_m: float = 0.0,
tilted: bool = False,
ground_span: tuple[float, float] | None = None,
) -> None:
half_road = group["road_width_m"] / 2.0
# 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽).
@@ -195,6 +196,36 @@ class _SectionGeometry:
(edge_offset + outward * top, edge_z),
]
# 물넘이 낮은 쪽 연장(294-3) — 노면이 내려가는 쪽 노면 끝 원지반이 노면보다 높으면(절토)
# 노면을 같은 기울기로 늘여 원지반과 처음 만나는 자리까지(그 쪽 절토 사면 없음) · 반폭
# (`ground_span`) 안에서 못 만나면 옛 길(절토 사면). ⚠ TS 짝 `SectionGeometry.reach`.
self.reach: dict[str, float] = {}
if tilted and ground_at is not None and abs(self.slope_per_offset) > 1e-12:
self._extend_low_side(ground_at, ground_span)
def _extend_low_side(
self, ground_at: Callable[[float], float], span: tuple[float, float] | None
) -> None:
side = "left" if self.slope_per_offset < 0 else "right"
sign = 1.0 if side == "left" else -1.0
extent = self.left_extent if side == "left" else self.right_extent
limit = (span[1] if side == "left" else -span[0]) if span else extent + 50.0
def gap(dist: float) -> float: # > 0 = 원지반이 늘인 노면 위(절토)
return ground_at(sign * dist) - self.road_z(sign * dist)
if gap(extent) <= 1e-3:
return
step = 0.05
dist = extent
while dist < limit - 1e-9:
nxt = min(dist + step, limit)
g0, g1 = gap(dist), gap(nxt)
if g1 <= 0:
self.reach[side] = dist + (nxt - dist) * g0 / (g0 - g1)
return
dist = nxt
def road_z(self, offset_m: float) -> float:
"""노면(노견 포함) 설계고 — 중심 계획고에서 횡단경사로 기운 단일 평면."""
return self.z_center + self.slope_per_offset * offset_m
@@ -430,6 +461,9 @@ class _SectionGeometry:
return points[-1][1]
role = self.left_role if side == "left" else self.right_role
dist = abs(offset_m)
reach = self.reach.get(side)
if reach is not None: # 늘인 노면 → 그 바깥은 원지반(294-3)
return self.road_z(offset_m) if dist <= reach + 1e-9 else ground_m
if role == "cut":
# 지반과 1회 교차하면 그 이후 절토는 의미 없음 → 지반 추종(N-2-4).
cross = self.cut_cross_dist(side)
@@ -468,6 +502,9 @@ class _SectionGeometry:
if cross is not None and offset > cross + 1e-9:
break
points.append(offset if side == "left" else -offset)
# 늘인 노면 끝(294-3)
for side, reach in self.reach.items():
points.append(reach if side == "left" else -reach)
# 절·성토 사면과 지반의 **첫** 교차점을 꼭짓점에 넣어 면적 절단을 정확히 한다(N-2-4).
for side in ("left", "right"):
role = self.left_role if side == "left" else self.right_role